1# Copyright 2015-2016 OpenMarket Ltd 2# Copyright 2017-2018 New Vector Ltd 3# Copyright 2019 The Matrix.org Foundation C.I.C. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17# This file can't be called email.py because if it is, we cannot: 18import email.utils 19import logging 20import os 21from enum import Enum 22 23import attr 24 25from ._base import Config, ConfigError 26 27logger = logging.getLogger(__name__) 28 29MISSING_PASSWORD_RESET_CONFIG_ERROR = """\ 30Password reset emails are enabled on this homeserver due to a partial 31'email' block. However, the following required keys are missing: 32 %s 33""" 34 35DEFAULT_SUBJECTS = { 36 "message_from_person_in_room": "[%(app)s] You have a message on %(app)s from %(person)s in the %(room)s room...", 37 "message_from_person": "[%(app)s] You have a message on %(app)s from %(person)s...", 38 "messages_from_person": "[%(app)s] You have messages on %(app)s from %(person)s...", 39 "messages_in_room": "[%(app)s] You have messages on %(app)s in the %(room)s room...", 40 "messages_in_room_and_others": "[%(app)s] You have messages on %(app)s in the %(room)s room and others...", 41 "messages_from_person_and_others": "[%(app)s] You have messages on %(app)s from %(person)s and others...", 42 "invite_from_person": "[%(app)s] %(person)s has invited you to chat on %(app)s...", 43 "invite_from_person_to_room": "[%(app)s] %(person)s has invited you to join the %(room)s room on %(app)s...", 44 "invite_from_person_to_space": "[%(app)s] %(person)s has invited you to join the %(space)s space on %(app)s...", 45 "password_reset": "[%(server_name)s] Password reset", 46 "email_validation": "[%(server_name)s] Validate your email", 47} 48 49LEGACY_TEMPLATE_DIR_WARNING = """ 50This server's configuration file is using the deprecated 'template_dir' setting in the 51'email' section. Support for this setting has been deprecated and will be removed in a 52future version of Synapse. Server admins should instead use the new 53'custom_templates_directory' setting documented here: 54https://matrix-org.github.io/synapse/latest/templates.html 55---------------------------------------------------------------------------------------""" 56 57 58@attr.s(slots=True, frozen=True) 59class EmailSubjectConfig: 60 message_from_person_in_room = attr.ib(type=str) 61 message_from_person = attr.ib(type=str) 62 messages_from_person = attr.ib(type=str) 63 messages_in_room = attr.ib(type=str) 64 messages_in_room_and_others = attr.ib(type=str) 65 messages_from_person_and_others = attr.ib(type=str) 66 invite_from_person = attr.ib(type=str) 67 invite_from_person_to_room = attr.ib(type=str) 68 invite_from_person_to_space = attr.ib(type=str) 69 password_reset = attr.ib(type=str) 70 email_validation = attr.ib(type=str) 71 72 73class EmailConfig(Config): 74 section = "email" 75 76 def read_config(self, config, **kwargs): 77 # TODO: We should separate better the email configuration from the notification 78 # and account validity config. 79 80 self.email_enable_notifs = False 81 82 email_config = config.get("email") 83 if email_config is None: 84 email_config = {} 85 86 self.email_smtp_host = email_config.get("smtp_host", "localhost") 87 self.email_smtp_port = email_config.get("smtp_port", 25) 88 self.email_smtp_user = email_config.get("smtp_user", None) 89 self.email_smtp_pass = email_config.get("smtp_pass", None) 90 self.require_transport_security = email_config.get( 91 "require_transport_security", False 92 ) 93 self.enable_smtp_tls = email_config.get("enable_tls", True) 94 if self.require_transport_security and not self.enable_smtp_tls: 95 raise ConfigError( 96 "email.require_transport_security requires email.enable_tls to be true" 97 ) 98 99 if "app_name" in email_config: 100 self.email_app_name = email_config["app_name"] 101 else: 102 self.email_app_name = "Matrix" 103 104 # TODO: Rename notif_from to something more generic, or have a separate 105 # from for password resets, message notifications, etc? 106 # Currently the email section is a bit bogged down with settings for 107 # multiple functions. Would be good to split it out into separate 108 # sections and only put the common ones under email: 109 self.email_notif_from = email_config.get("notif_from", None) 110 if self.email_notif_from is not None: 111 # make sure it's valid 112 parsed = email.utils.parseaddr(self.email_notif_from) 113 if parsed[1] == "": 114 raise RuntimeError("Invalid notif_from address") 115 116 # A user-configurable template directory 117 template_dir = email_config.get("template_dir") 118 if template_dir is not None: 119 logger.warning(LEGACY_TEMPLATE_DIR_WARNING) 120 121 if isinstance(template_dir, str): 122 # We need an absolute path, because we change directory after starting (and 123 # we don't yet know what auxiliary templates like mail.css we will need). 124 template_dir = os.path.abspath(template_dir) 125 elif template_dir is not None: 126 # If template_dir is something other than a str or None, warn the user 127 raise ConfigError("Config option email.template_dir must be type str") 128 129 self.email_enable_notifs = email_config.get("enable_notifs", False) 130 131 self.threepid_behaviour_email = ( 132 # Have Synapse handle the email sending if account_threepid_delegates.email 133 # is not defined 134 # msisdn is currently always remote while Synapse does not support any method of 135 # sending SMS messages 136 ThreepidBehaviour.REMOTE 137 if self.root.registration.account_threepid_delegate_email 138 else ThreepidBehaviour.LOCAL 139 ) 140 141 if config.get("trust_identity_server_for_password_resets"): 142 raise ConfigError( 143 'The config option "trust_identity_server_for_password_resets" ' 144 'has been replaced by "account_threepid_delegate". ' 145 "Please consult the sample config at docs/sample_config.yaml for " 146 "details and update your config file." 147 ) 148 149 self.local_threepid_handling_disabled_due_to_email_config = False 150 if ( 151 self.threepid_behaviour_email == ThreepidBehaviour.LOCAL 152 and email_config == {} 153 ): 154 # We cannot warn the user this has happened here 155 # Instead do so when a user attempts to reset their password 156 self.local_threepid_handling_disabled_due_to_email_config = True 157 158 self.threepid_behaviour_email = ThreepidBehaviour.OFF 159 160 # Get lifetime of a validation token in milliseconds 161 self.email_validation_token_lifetime = self.parse_duration( 162 email_config.get("validation_token_lifetime", "1h") 163 ) 164 165 if self.threepid_behaviour_email == ThreepidBehaviour.LOCAL: 166 missing = [] 167 if not self.email_notif_from: 168 missing.append("email.notif_from") 169 170 if missing: 171 raise ConfigError( 172 MISSING_PASSWORD_RESET_CONFIG_ERROR % (", ".join(missing),) 173 ) 174 175 # These email templates have placeholders in them, and thus must be 176 # parsed using a templating engine during a request 177 password_reset_template_html = email_config.get( 178 "password_reset_template_html", "password_reset.html" 179 ) 180 password_reset_template_text = email_config.get( 181 "password_reset_template_text", "password_reset.txt" 182 ) 183 registration_template_html = email_config.get( 184 "registration_template_html", "registration.html" 185 ) 186 registration_template_text = email_config.get( 187 "registration_template_text", "registration.txt" 188 ) 189 add_threepid_template_html = email_config.get( 190 "add_threepid_template_html", "add_threepid.html" 191 ) 192 add_threepid_template_text = email_config.get( 193 "add_threepid_template_text", "add_threepid.txt" 194 ) 195 196 password_reset_template_failure_html = email_config.get( 197 "password_reset_template_failure_html", "password_reset_failure.html" 198 ) 199 registration_template_failure_html = email_config.get( 200 "registration_template_failure_html", "registration_failure.html" 201 ) 202 add_threepid_template_failure_html = email_config.get( 203 "add_threepid_template_failure_html", "add_threepid_failure.html" 204 ) 205 206 # These templates do not support any placeholder variables, so we 207 # will read them from disk once during setup 208 password_reset_template_success_html = email_config.get( 209 "password_reset_template_success_html", "password_reset_success.html" 210 ) 211 registration_template_success_html = email_config.get( 212 "registration_template_success_html", "registration_success.html" 213 ) 214 add_threepid_template_success_html = email_config.get( 215 "add_threepid_template_success_html", "add_threepid_success.html" 216 ) 217 218 # Read all templates from disk 219 ( 220 self.email_password_reset_template_html, 221 self.email_password_reset_template_text, 222 self.email_registration_template_html, 223 self.email_registration_template_text, 224 self.email_add_threepid_template_html, 225 self.email_add_threepid_template_text, 226 self.email_password_reset_template_confirmation_html, 227 self.email_password_reset_template_failure_html, 228 self.email_registration_template_failure_html, 229 self.email_add_threepid_template_failure_html, 230 password_reset_template_success_html_template, 231 registration_template_success_html_template, 232 add_threepid_template_success_html_template, 233 ) = self.read_templates( 234 [ 235 password_reset_template_html, 236 password_reset_template_text, 237 registration_template_html, 238 registration_template_text, 239 add_threepid_template_html, 240 add_threepid_template_text, 241 "password_reset_confirmation.html", 242 password_reset_template_failure_html, 243 registration_template_failure_html, 244 add_threepid_template_failure_html, 245 password_reset_template_success_html, 246 registration_template_success_html, 247 add_threepid_template_success_html, 248 ], 249 ( 250 td 251 for td in ( 252 self.root.server.custom_template_directory, 253 template_dir, 254 ) 255 if td 256 ), # Filter out template_dir if not provided 257 ) 258 259 # Render templates that do not contain any placeholders 260 self.email_password_reset_template_success_html_content = ( 261 password_reset_template_success_html_template.render() 262 ) 263 self.email_registration_template_success_html_content = ( 264 registration_template_success_html_template.render() 265 ) 266 self.email_add_threepid_template_success_html_content = ( 267 add_threepid_template_success_html_template.render() 268 ) 269 270 if self.email_enable_notifs: 271 missing = [] 272 if not self.email_notif_from: 273 missing.append("email.notif_from") 274 275 if missing: 276 raise ConfigError( 277 "email.enable_notifs is True but required keys are missing: %s" 278 % (", ".join(missing),) 279 ) 280 281 notif_template_html = email_config.get( 282 "notif_template_html", "notif_mail.html" 283 ) 284 notif_template_text = email_config.get( 285 "notif_template_text", "notif_mail.txt" 286 ) 287 288 ( 289 self.email_notif_template_html, 290 self.email_notif_template_text, 291 ) = self.read_templates( 292 [notif_template_html, notif_template_text], 293 ( 294 td 295 for td in ( 296 self.root.server.custom_template_directory, 297 template_dir, 298 ) 299 if td 300 ), # Filter out template_dir if not provided 301 ) 302 303 self.email_notif_for_new_users = email_config.get( 304 "notif_for_new_users", True 305 ) 306 self.email_riot_base_url = email_config.get( 307 "client_base_url", email_config.get("riot_base_url", None) 308 ) 309 310 if self.root.account_validity.account_validity_renew_by_email_enabled: 311 expiry_template_html = email_config.get( 312 "expiry_template_html", "notice_expiry.html" 313 ) 314 expiry_template_text = email_config.get( 315 "expiry_template_text", "notice_expiry.txt" 316 ) 317 318 ( 319 self.account_validity_template_html, 320 self.account_validity_template_text, 321 ) = self.read_templates( 322 [expiry_template_html, expiry_template_text], 323 ( 324 td 325 for td in ( 326 self.root.server.custom_template_directory, 327 template_dir, 328 ) 329 if td 330 ), # Filter out template_dir if not provided 331 ) 332 333 subjects_config = email_config.get("subjects", {}) 334 subjects = {} 335 336 for key, default in DEFAULT_SUBJECTS.items(): 337 subjects[key] = subjects_config.get(key, default) 338 339 self.email_subjects = EmailSubjectConfig(**subjects) 340 341 # The invite client location should be a HTTP(S) URL or None. 342 self.invite_client_location = email_config.get("invite_client_location") or None 343 if self.invite_client_location: 344 if not isinstance(self.invite_client_location, str): 345 raise ConfigError( 346 "Config option email.invite_client_location must be type str" 347 ) 348 if not ( 349 self.invite_client_location.startswith("http://") 350 or self.invite_client_location.startswith("https://") 351 ): 352 raise ConfigError( 353 "Config option email.invite_client_location must be a http or https URL", 354 path=("email", "invite_client_location"), 355 ) 356 357 def generate_config_section(self, config_dir_path, server_name, **kwargs): 358 return ( 359 """\ 360 # Configuration for sending emails from Synapse. 361 # 362 # Server admins can configure custom templates for email content. See 363 # https://matrix-org.github.io/synapse/latest/templates.html for more information. 364 # 365 email: 366 # The hostname of the outgoing SMTP server to use. Defaults to 'localhost'. 367 # 368 #smtp_host: mail.server 369 370 # The port on the mail server for outgoing SMTP. Defaults to 25. 371 # 372 #smtp_port: 587 373 374 # Username/password for authentication to the SMTP server. By default, no 375 # authentication is attempted. 376 # 377 #smtp_user: "exampleusername" 378 #smtp_pass: "examplepassword" 379 380 # Uncomment the following to require TLS transport security for SMTP. 381 # By default, Synapse will connect over plain text, and will then switch to 382 # TLS via STARTTLS *if the SMTP server supports it*. If this option is set, 383 # Synapse will refuse to connect unless the server supports STARTTLS. 384 # 385 #require_transport_security: true 386 387 # Uncomment the following to disable TLS for SMTP. 388 # 389 # By default, if the server supports TLS, it will be used, and the server 390 # must present a certificate that is valid for 'smtp_host'. If this option 391 # is set to false, TLS will not be used. 392 # 393 #enable_tls: false 394 395 # notif_from defines the "From" address to use when sending emails. 396 # It must be set if email sending is enabled. 397 # 398 # The placeholder '%%(app)s' will be replaced by the application name, 399 # which is normally 'app_name' (below), but may be overridden by the 400 # Matrix client application. 401 # 402 # Note that the placeholder must be written '%%(app)s', including the 403 # trailing 's'. 404 # 405 #notif_from: "Your Friendly %%(app)s homeserver <noreply@example.com>" 406 407 # app_name defines the default value for '%%(app)s' in notif_from and email 408 # subjects. It defaults to 'Matrix'. 409 # 410 #app_name: my_branded_matrix_server 411 412 # Uncomment the following to enable sending emails for messages that the user 413 # has missed. Disabled by default. 414 # 415 #enable_notifs: true 416 417 # Uncomment the following to disable automatic subscription to email 418 # notifications for new users. Enabled by default. 419 # 420 #notif_for_new_users: false 421 422 # Custom URL for client links within the email notifications. By default 423 # links will be based on "https://matrix.to". 424 # 425 # (This setting used to be called riot_base_url; the old name is still 426 # supported for backwards-compatibility but is now deprecated.) 427 # 428 #client_base_url: "http://localhost/riot" 429 430 # Configure the time that a validation email will expire after sending. 431 # Defaults to 1h. 432 # 433 #validation_token_lifetime: 15m 434 435 # The web client location to direct users to during an invite. This is passed 436 # to the identity server as the org.matrix.web_client_location key. Defaults 437 # to unset, giving no guidance to the identity server. 438 # 439 #invite_client_location: https://app.element.io 440 441 # Subjects to use when sending emails from Synapse. 442 # 443 # The placeholder '%%(app)s' will be replaced with the value of the 'app_name' 444 # setting above, or by a value dictated by the Matrix client application. 445 # 446 # If a subject isn't overridden in this configuration file, the value used as 447 # its example will be used. 448 # 449 #subjects: 450 451 # Subjects for notification emails. 452 # 453 # On top of the '%%(app)s' placeholder, these can use the following 454 # placeholders: 455 # 456 # * '%%(person)s', which will be replaced by the display name of the user(s) 457 # that sent the message(s), e.g. "Alice and Bob". 458 # * '%%(room)s', which will be replaced by the name of the room the 459 # message(s) have been sent to, e.g. "My super room". 460 # 461 # See the example provided for each setting to see which placeholder can be 462 # used and how to use them. 463 # 464 # Subject to use to notify about one message from one or more user(s) in a 465 # room which has a name. 466 #message_from_person_in_room: "%(message_from_person_in_room)s" 467 # 468 # Subject to use to notify about one message from one or more user(s) in a 469 # room which doesn't have a name. 470 #message_from_person: "%(message_from_person)s" 471 # 472 # Subject to use to notify about multiple messages from one or more users in 473 # a room which doesn't have a name. 474 #messages_from_person: "%(messages_from_person)s" 475 # 476 # Subject to use to notify about multiple messages in a room which has a 477 # name. 478 #messages_in_room: "%(messages_in_room)s" 479 # 480 # Subject to use to notify about multiple messages in multiple rooms. 481 #messages_in_room_and_others: "%(messages_in_room_and_others)s" 482 # 483 # Subject to use to notify about multiple messages from multiple persons in 484 # multiple rooms. This is similar to the setting above except it's used when 485 # the room in which the notification was triggered has no name. 486 #messages_from_person_and_others: "%(messages_from_person_and_others)s" 487 # 488 # Subject to use to notify about an invite to a room which has a name. 489 #invite_from_person_to_room: "%(invite_from_person_to_room)s" 490 # 491 # Subject to use to notify about an invite to a room which doesn't have a 492 # name. 493 #invite_from_person: "%(invite_from_person)s" 494 495 # Subject for emails related to account administration. 496 # 497 # On top of the '%%(app)s' placeholder, these one can use the 498 # '%%(server_name)s' placeholder, which will be replaced by the value of the 499 # 'server_name' setting in your Synapse configuration. 500 # 501 # Subject to use when sending a password reset email. 502 #password_reset: "%(password_reset)s" 503 # 504 # Subject to use when sending a verification email to assert an address's 505 # ownership. 506 #email_validation: "%(email_validation)s" 507 """ 508 % DEFAULT_SUBJECTS 509 ) 510 511 512class ThreepidBehaviour(Enum): 513 """ 514 Enum to define the behaviour of Synapse with regards to when it contacts an identity 515 server for 3pid registration and password resets 516 517 REMOTE = use an external server to send tokens 518 LOCAL = send tokens ourselves 519 OFF = disable registration via 3pid and password resets 520 """ 521 522 REMOTE = "remote" 523 LOCAL = "local" 524 OFF = "off" 525