def requestToken(self, emailAddress, clientSecret, sendAttempt, nextLink, ipaddress=None): valSessionStore = ThreePidValSessionStore(self.sydent) valSession = valSessionStore.getOrCreateTokenSession(medium='email', address=emailAddress, clientSecret=clientSecret) valSessionStore.setMtime(valSession.id, time_msec()) if int(valSession.sendAttemptNumber) >= int(sendAttempt): logger.info("Not mailing code because current send attempt (%d) is not less than given send attempt (%s)", int(sendAttempt), int(valSession.sendAttemptNumber)) return valSession.id ipstring = ipaddress if ipaddress else u"an unknown location" substitutions = { 'ipaddress': ipstring, 'link': self.makeValidateLink(valSession, clientSecret, nextLink), 'token': valSession.token, } logger.info( "Attempting to mail code %s (nextLink: %s) to %s", valSession.token, nextLink, emailAddress, ) sendEmail(self.sydent, 'email.template', emailAddress, substitutions) valSessionStore.setSendAttemptNumber(valSession.id, sendAttempt) return valSession.id
def test_migration_email(self): with patch("sydent.util.emailutils.smtplib") as smtplib: # self.sydent.config.email.template is deprecated if self.sydent.config.email.template is None: templateFile = self.sydent.get_branded_template( None, "migration_template.eml", ) else: templateFile = self.sydent.config.email.template sendEmail( self.sydent, templateFile, "*****@*****.**", { "mxid": "@bob:example.com", "subject_header_value": "MatrixID Deletion", }, ) smtp = smtplib.SMTP.return_value email_contents = smtp.sendmail.call_args[0][2].decode("utf-8") self.assertIn("In the past", email_contents) # test email was sent smtp.sendmail.assert_called()
def test_jinja_vector_verification(self): substitutions = { "address": "*****@*****.**", "medium": "email", "to": "*****@*****.**", "link": "https://link_test.com", } templateFile = self.sydent.get_branded_template( "vector-im", "verification_template.eml", ) with patch("sydent.util.emailutils.smtplib") as smtplib: sendEmail(self.sydent, templateFile, "*****@*****.**", substitutions) smtp = smtplib.SMTP.return_value email_contents = smtp.sendmail.call_args[0][2].decode("utf-8") path = os.path.join( self.sydent.config.general.templates_path, "vector_verification_sample.txt", ) with open(path, "r") as file: expected_text = file.read() # remove the email headers as they are variable email_contents = email_contents[email_contents.index("Hello"):] # test all ouput is as expected self.assertEqual(email_contents, expected_text)
def test_jinja_matrix_verification(self): substitutions = { "address": "*****@*****.**", "medium": "email", "to": "*****@*****.**", "token": "<<token>>", "link": "https://link_test.com", } templateFile = self.sydent.get_branded_template( "matrix-org", "verification_template.eml", ) with patch("sydent.util.emailutils.smtplib") as smtplib: sendEmail(self.sydent, templateFile, "*****@*****.**", substitutions) smtp = smtplib.SMTP.return_value email_contents = smtp.sendmail.call_args[0][2].decode("utf-8") # test html input is escaped self.assertIn("<<token>>", email_contents) # test safe values are not escaped self.assertIn("<<token>>", email_contents)
def requestToken(self, emailAddress, clientSecret, sendAttempt, nextLink, ipaddress=None): """ Creates or retrieves a validation session and sends an email to the corresponding email address with a token to use to verify the association. :param emailAddress: The email address to send the email to. :type emailAddress: unicode :param clientSecret: The client secret to use. :type clientSecret: unicode :param sendAttempt: The current send attempt. :type sendAttempt: int :param nextLink: The link to redirect the user to once they have completed the validation. :type nextLink: unicode :param ipaddress: The requester's IP address. :type ipaddress: str or None :return: The ID of the session created (or of the existing one if any) :rtype: int """ valSessionStore = ThreePidValSessionStore(self.sydent) valSession = valSessionStore.getOrCreateTokenSession( medium=u'email', address=emailAddress, clientSecret=clientSecret) valSessionStore.setMtime(valSession.id, time_msec()) if int(valSession.sendAttemptNumber) >= int(sendAttempt): logger.info( "Not mailing code because current send attempt (%d) is not less than given send attempt (%s)", int(sendAttempt), int(valSession.sendAttemptNumber)) return valSession.id ipstring = ipaddress if ipaddress else u"an unknown location" substitutions = { 'ipaddress': ipstring, 'link': self.makeValidateLink(valSession, clientSecret, nextLink), 'token': valSession.token, } logger.info( "Attempting to mail code %s (nextLink: %s) to %s", valSession.token, nextLink, emailAddress, ) sendEmail(self.sydent, 'email.template', emailAddress, substitutions) valSessionStore.setSendAttemptNumber(valSession.id, sendAttempt) return valSession.id
def sendEmailWithBackoff( sydent: Sydent, address: str, mxid: str, test: bool = False, ) -> None: """Send an email with exponential backoff - that way we don't stop sending halfway through if the SMTP server rejects our email (e.g. because of rate limiting). Setting test to True disables the backoff. Raises a CantSendEmailException if no email could be sent after MAX_ATTEMPTS_FOR_EMAIL attempts. """ # Disable backoff if we're running tests. backoff = 1 if not test else 0 for i in range(MAX_ATTEMPTS_FOR_EMAIL): try: template_file = sydent.get_branded_template( None, "migration_template.eml", ) sendEmail( sydent, template_file, address, {"mxid": mxid}, log_send_errors=False, ) logger.info("Sent email to %s" % address) return except EmailSendException: logger.info( "Failed to send email to %s (attempt %d/%d)" % (address, i + 1, MAX_ATTEMPTS_FOR_EMAIL) ) time.sleep(backoff) backoff *= 2 raise CantSendEmailException()
def requestToken(self, emailAddress, clientSecret, sendAttempt, nextLink, ipaddress=None): valSessionStore = ThreePidValSessionStore(self.sydent) valSession = valSessionStore.getOrCreateTokenSession( medium='email', address=emailAddress, clientSecret=clientSecret) valSessionStore.setMtime(valSession.id, time_msec()) if int(valSession.sendAttemptNumber) >= int(sendAttempt): logger.info( "Not mailing code because current send attempt (%d) is not less than given send attempt (%s)", int(sendAttempt), int(valSession.sendAttemptNumber)) return valSession.id ipstring = ipaddress if ipaddress else u"an unknown location" substitutions = { 'ipaddress': ipstring, 'link': self.makeValidateLink(valSession, clientSecret, nextLink), 'token': valSession.token, } logger.info( "Attempting to mail code %s (nextLink: %s) to %s", valSession.token, nextLink, emailAddress, ) sendEmail(self.sydent, 'email.template', emailAddress, substitutions) valSessionStore.setSendAttemptNumber(valSession.id, sendAttempt) return valSession.id
def render_POST(self, request): send_cors(request) err, args = get_args(request, ("medium", "address", "room_id", "sender",)) if err: return json.dumps(err) medium = args["medium"] address = args["address"] roomId = args["room_id"] sender = args["sender"] globalAssocStore = GlobalAssociationStore(self.sydent) mxid = globalAssocStore.getMxid(medium, address) if mxid: request.setResponseCode(400) return json.dumps({ "errcode": "THREEPID_IN_USE", "error": "Binding already known", "mxid": mxid, }) if medium != "email": request.setResponseCode(400) return json.dumps({ "errcode": "M_UNRECOGNIZED", "error": "Didn't understand medium '%s'" % (medium,), }) token = self._randomString(128) tokenStore = JoinTokenStore(self.sydent) ephemeralPrivateKey = nacl.signing.SigningKey.generate() ephemeralPublicKey = ephemeralPrivateKey.verify_key ephemeralPrivateKeyBase64 = encode_base64(ephemeralPrivateKey.encode(), True) ephemeralPublicKeyBase64 = encode_base64(ephemeralPublicKey.encode(), True) tokenStore.storeEphemeralPublicKey(ephemeralPublicKeyBase64) tokenStore.storeToken(medium, address, roomId, sender, token) substitutions = {} for key, values in request.args.items(): if len(values) == 1 and type(values[0]) == str: substitutions[key] = values[0] substitutions["token"] = token required = [ 'sender_display_name', 'token', 'room_name', 'bracketed_room_name', 'room_avatar_url', 'sender_display_name', 'guest_user_id', 'guest_access_token', ] for k in required: substitutions.setdefault(k, '') substitutions["ephemeral_private_key"] = ephemeralPrivateKeyBase64 if substitutions["room_name"] != '': substitutions["bracketed_room_name"] = "(%s)" % substitutions["room_name"] subject_header = Header(self.sydent.cfg.get('email', 'email.invite.subject', raw=True) % substitutions, 'utf8') substitutions["subject_header_value"] = subject_header.encode() sendEmail(self.sydent, "email.invite_template", address, substitutions) pubKey = self.sydent.keyring.ed25519.verify_key pubKeyBase64 = encode_base64(pubKey.encode()) baseUrl = "%s/_matrix/identity/api/v1" % (self.sydent.cfg.get('http', 'client_http_base'),) keysToReturn = [] keysToReturn.append({ "public_key": pubKeyBase64, "key_validity_url": baseUrl + "/pubkey/isvalid", }) keysToReturn.append({ "public_key": ephemeralPublicKeyBase64, "key_validity_url": baseUrl + "/pubkey/ephemeral/isvalid", }) resp = { "token": token, "public_key": pubKeyBase64, "public_keys": keysToReturn, "display_name": self.redact(address), } return json.dumps(resp)
def render_POST(self, request): send_cors(request) err = require_args(request, ("medium", "address", "room_id", "sender",)) if err: return json.dumps(err) medium = request.args["medium"][0] address = request.args["address"][0] roomId = request.args["room_id"][0] sender = request.args["sender"][0] globalAssocStore = GlobalAssociationStore(self.sydent) mxid = globalAssocStore.getMxid(medium, address) if mxid: request.setResponseCode(400) return json.dumps({ "errcode": "THREEPID_IN_USE", "error": "Binding already known", "mxid": mxid, }) if medium != "email": request.setResponseCode(400) return json.dumps({ "errcode": "M_UNRECOGNIZED", "error": "Didn't understand medium '%s'" % (medium,), }) token = self._randomString(128) tokenStore = JoinTokenStore(self.sydent) ephemeralPrivateKey = nacl.signing.SigningKey.generate() ephemeralPublicKey = ephemeralPrivateKey.verify_key ephemeralPrivateKeyBase64 = encode_base64(ephemeralPrivateKey.encode(), True) ephemeralPublicKeyBase64 = encode_base64(ephemeralPublicKey.encode(), True) tokenStore.storeEphemeralPublicKey(ephemeralPublicKeyBase64) tokenStore.storeToken(medium, address, roomId, sender, token) substitutions = {} for key, values in request.args.items(): if len(values) == 1 and type(values[0]) == str: substitutions[key] = values[0] substitutions["token"] = token required = [ 'sender_display_name', 'token', 'room_name', 'bracketed_room_name', 'room_avatar_url', 'sender_display_name', 'guest_user_id', 'guest_access_token', ] for k in required: substitutions.setdefault(k, '') substitutions["ephemeral_private_key"] = ephemeralPrivateKeyBase64 if substitutions["room_name"] != '': substitutions["bracketed_room_name"] = "(%s)" % substitutions["room_name"] subject_header = Header(self.sydent.cfg.get('email', 'email.invite.subject', raw=True) % substitutions, 'utf8') substitutions["subject_header_value"] = subject_header.encode() sendEmail(self.sydent, "email.invite_template", address, substitutions) pubKey = self.sydent.keyring.ed25519.verify_key pubKeyBase64 = encode_base64(pubKey.encode()) baseUrl = "%s/_matrix/identity/api/v1" % (self.sydent.cfg.get('http', 'client_http_base'),) keysToReturn = [] keysToReturn.append({ "public_key": pubKeyBase64, "key_validity_url": baseUrl + "/pubkey/isvalid", }) keysToReturn.append({ "public_key": ephemeralPublicKeyBase64, "key_validity_url": baseUrl + "/pubkey/ephemeral/isvalid", }) resp = { "token": token, "public_key": pubKeyBase64, "public_keys": keysToReturn, "display_name": self.redact(address), } return json.dumps(resp)
def render_POST(self, request): send_cors(request) authIfV2(self.sydent, request) args = get_args(request, ("medium", "address", "room_id", "sender",)) medium = args["medium"] address = args["address"] roomId = args["room_id"] sender = args["sender"] globalAssocStore = GlobalAssociationStore(self.sydent) mxid = globalAssocStore.getMxid(medium, address) if mxid: request.setResponseCode(400) return { "errcode": "M_THREEPID_IN_USE", "error": "Binding already known", "mxid": mxid, } if medium != "email": request.setResponseCode(400) return { "errcode": "M_UNRECOGNIZED", "error": "Didn't understand medium '%s'" % (medium,), } token = self._randomString(128) tokenStore = JoinTokenStore(self.sydent) ephemeralPrivateKey = nacl.signing.SigningKey.generate() ephemeralPublicKey = ephemeralPrivateKey.verify_key ephemeralPrivateKeyBase64 = encode_base64(ephemeralPrivateKey.encode(), True) ephemeralPublicKeyBase64 = encode_base64(ephemeralPublicKey.encode(), True) tokenStore.storeEphemeralPublicKey(ephemeralPublicKeyBase64) tokenStore.storeToken(medium, address, roomId, sender, token) # Variables to substitute in the template. substitutions = {} # Include all arguments sent via the request. for k, v in args.items(): if isinstance(v, string_types): substitutions[k] = v substitutions["token"] = token # Substitutions that the template requires, but are optional to provide # to the API. extra_substitutions = [ 'sender_display_name', 'token', 'room_name', 'bracketed_room_name', 'room_avatar_url', 'sender_avatar_url', 'guest_user_id', 'guest_access_token', ] for k in extra_substitutions: substitutions.setdefault(k, '') substitutions["ephemeral_private_key"] = ephemeralPrivateKeyBase64 if substitutions["room_name"] != '': substitutions["bracketed_room_name"] = "(%s)" % substitutions["room_name"] substitutions["web_client_location"] = self.sydent.default_web_client_location if 'org.matrix.web_client_location' in substitutions: substitutions["web_client_location"] = substitutions.pop("org.matrix.web_client_location") subject_header = Header(self.sydent.cfg.get('email', 'email.invite.subject', raw=True) % substitutions, 'utf8') substitutions["subject_header_value"] = subject_header.encode() brand = self.sydent.brand_from_request(request) templateFile = self.sydent.get_branded_template( brand, "invite_template.eml", ('email', 'email.invite_template'), ) sendEmail(self.sydent, templateFile, address, substitutions) pubKey = self.sydent.keyring.ed25519.verify_key pubKeyBase64 = encode_base64(pubKey.encode()) baseUrl = "%s/_matrix/identity/api/v1" % (self.sydent.cfg.get('http', 'client_http_base'),) keysToReturn = [] keysToReturn.append({ "public_key": pubKeyBase64, "key_validity_url": baseUrl + "/pubkey/isvalid", }) keysToReturn.append({ "public_key": ephemeralPublicKeyBase64, "key_validity_url": baseUrl + "/pubkey/ephemeral/isvalid", }) resp = { "token": token, "public_key": pubKeyBase64, "public_keys": keysToReturn, "display_name": self.redact_email_address(address), } return resp
def render_POST(self, request: Request) -> JsonDict: send_cors(request) args = get_args( request, ( "medium", "address", "room_id", "sender", ), ) medium = args["medium"] address = args["address"] roomId = args["room_id"] sender = args["sender"] # ensure we are casefolding email address before storing normalised_address = normalise_address(address, medium) verified_sender = None if self.require_auth: account = authV2(self.sydent, request) verified_sender = sender if account.userId != sender: raise MatrixRestError(403, "M_UNAUTHORIZED", "'sender' doesn't match") globalAssocStore = GlobalAssociationStore(self.sydent) mxid = globalAssocStore.getMxid(medium, normalised_address) if mxid: request.setResponseCode(400) return { "errcode": "M_THREEPID_IN_USE", "error": "Binding already known", "mxid": mxid, } if medium != "email": request.setResponseCode(400) return { "errcode": "M_UNRECOGNIZED", "error": "Didn't understand medium '%s'" % (medium, ), } if not (0 < len(address) <= MAX_EMAIL_ADDRESS_LENGTH): request.setResponseCode(400) return { "errcode": "M_INVALID_PARAM", "error": "Invalid email provided" } token = self._randomString(128) tokenStore = JoinTokenStore(self.sydent) ephemeralPrivateKey = nacl.signing.SigningKey.generate() ephemeralPublicKey = ephemeralPrivateKey.verify_key ephemeralPrivateKeyBase64 = encode_base64(ephemeralPrivateKey.encode(), True) ephemeralPublicKeyBase64 = encode_base64(ephemeralPublicKey.encode(), True) tokenStore.storeEphemeralPublicKey(ephemeralPublicKeyBase64) tokenStore.storeToken(medium, normalised_address, roomId, sender, token) # Variables to substitute in the template. substitutions = {} # Include all arguments sent via the request. for k, v in args.items(): if isinstance(v, str): substitutions[k] = v substitutions["token"] = token # Substitutions that the template requires, but are optional to provide # to the API. extra_substitutions = [ "sender_display_name", "token", "room_name", "bracketed_room_name", "room_avatar_url", "sender_avatar_url", "guest_user_id", "guest_access_token", ] for k in extra_substitutions: substitutions.setdefault(k, "") # For MSC3288 room type, prefer the stable field, but fallback to the # unstable field. if "room_type" not in substitutions: substitutions["room_type"] = substitutions.get( "org.matrix.msc3288.room_type", "") substitutions["bracketed_verified_sender"] = "" if verified_sender: substitutions["bracketed_verified_sender"] = "(%s) " % ( verified_sender, ) substitutions["ephemeral_private_key"] = ephemeralPrivateKeyBase64 if substitutions["room_name"] != "": substitutions[ "bracketed_room_name"] = "(%s) " % substitutions["room_name"] substitutions[ "web_client_location"] = self.sydent.config.email.default_web_client_location if "org.matrix.web_client_location" in substitutions: substitutions["web_client_location"] = substitutions[ "org.matrix.web_client_location"] if substitutions["room_type"] == "m.space": subject = self.sydent.config.email.invite_subject_space % substitutions else: subject = self.sydent.config.email.invite_subject % substitutions substitutions["subject_header_value"] = Header(subject, "utf8").encode() brand = self.sydent.brand_from_request(request) # self.sydent.config.email.invite_template is deprecated if self.sydent.config.email.invite_template is None: templateFile = self.sydent.get_branded_template( brand, "invite_template.eml", ) else: templateFile = self.sydent.config.email.invite_template try: sendEmail(self.sydent, templateFile, normalised_address, substitutions) except EmailAddressException: request.setResponseCode(HTTPStatus.BAD_REQUEST) return { "errcode": "M_INVALID_EMAIL", "error": "Invalid email address" } pubKey = self.sydent.keyring.ed25519.verify_key pubKeyBase64 = encode_base64(pubKey.encode()) baseUrl = "%s/_matrix/identity/api/v1" % ( self.sydent.config.http.server_http_url_base, ) keysToReturn = [] keysToReturn.append({ "public_key": pubKeyBase64, "key_validity_url": baseUrl + "/pubkey/isvalid", }) keysToReturn.append({ "public_key": ephemeralPublicKeyBase64, "key_validity_url": baseUrl + "/pubkey/ephemeral/isvalid", }) resp = { "token": token, "public_key": pubKeyBase64, "public_keys": keysToReturn, "display_name": self.redact_email_address(address), } return resp
def test_jinja_matrix_invite(self): substitutions = { "address": "*****@*****.**", "medium": "email", "room_alias": "#somewhere:exmaple.org", "room_avatar_url": "mxc://example.org/s0meM3dia", "room_id": "!something:example.org", "room_name": "Bob's Emporium of Messages", "sender": "@bob:example.com", "sender_avatar_url": "mxc://example.org/an0th3rM3dia", "sender_display_name": "<Bob Smith>", "bracketed_verified_sender": "Bob Smith", "bracketed_room_name": "Bob's Emporium of Messages", "to": "*****@*****.**", "token": "a_token", "ephemeral_private_key": "mystery_key", "web_client_location": "https://matrix.org", "room_type": "", } # self.sydent.config.email.invite_template is deprecated if self.sydent.config.email.invite_template is None: templateFile = self.sydent.get_branded_template( "matrix-org", "invite_template.eml", ) else: templateFile = self.sydent.config.email.invite_template with patch("sydent.util.emailutils.smtplib") as smtplib: sendEmail(self.sydent, templateFile, "*****@*****.**", substitutions) smtp = smtplib.SMTP.return_value email_contents = smtp.sendmail.call_args[0][2].decode("utf-8") # test url input is encoded self.assertIn(urllib.parse.quote("mxc://example.org/s0meM3dia"), email_contents) # test html input is escaped self.assertIn("Bob's Emporium of Messages", email_contents) # test safe values are not escaped self.assertIn("<Bob Smith>", email_contents) # test our link is as expected expected_url = ( "https://matrix.org/#/room/" + urllib.parse.quote("!something:example.org") + "?email=" + urllib.parse.quote("*****@*****.**") + "&signurl=https%3A%2F%2Fmatrix.org%2F_matrix%2Fidentity%2Fapi%2Fv1%2Fsign-ed25519%3Ftoken%3D" + urllib.parse.quote("a_token") + "%26private_key%3D" + urllib.parse.quote("mystery_key") + "&room_name=" + urllib.parse.quote("Bob's Emporium of Messages") + "&room_avatar_url=" + urllib.parse.quote("mxc://example.org/s0meM3dia") + "&inviter_name=" + urllib.parse.quote("<Bob Smith>") + "&guest_access_token=&guest_user_id=&room_type=") text = email_contents.splitlines() link = text[22] self.assertEqual(link, expected_url)