def getMxid(self, medium: str, normalised_address: str) -> Optional[str]: """ Retrieves the MXID associated with a 3PID. Please note that emails need to be casefolded before calling this function. :param medium: The medium of the 3PID. :param normalised_address: The address of the 3PID. :return: The associated MXID, or None if no MXID is associated with this 3PID. """ cur = self.sydent.db.cursor() res = cur.execute( "select mxid from global_threepid_associations where " "medium = ? and lower(address) = lower(?) and notBefore < ? and notAfter > ? " "order by ts desc limit 1", (medium, normalised_address, time_msec(), time_msec()), ) row: Tuple[Optional[str]] = res.fetchone() if not row: return None return row[0]
def signedAssociationStringForThreepid(self, medium: str, address: str) -> Optional[str]: """ Retrieve the JSON for the signed association matching the provided 3PID, if one exists. :param medium: The medium of the 3PID. :param address: The address of the 3PID. :return: The signed association, or None if no association was found for this 3PID. """ cur = self.sydent.db.cursor() # We treat address as case-insensitive because that's true for all the # threepids we have currently (we treat the local part of email addresses as # case insensitive which is technically incorrect). If we someday get a # case-sensitive threepid, this can change. res = cur.execute( "select sgAssoc from global_threepid_associations where " "medium = ? and lower(address) = lower(?) and notBefore < ? and notAfter > ? " "order by ts desc limit 1", (medium, address, time_msec(), time_msec()), ) row: Optional[Tuple[str]] = res.fetchone() if not row: return None sgAssocStr = row[0] return sgAssocStr
def getOrCreateTokenSession(self, medium, address, clientSecret): cur = self.sydent.db.cursor() cur.execute("select s.id, s.medium, s.address, s.clientSecret, s.validated, s.mtime, " "t.token, t.sendAttemptNumber from threepid_validation_sessions s,threepid_token_auths t " "where s.medium = ? and s.address = ? and s.clientSecret = ? and t.validationSession = s.id", (medium, address, clientSecret)) row = cur.fetchone() if row: s = ValidationSession(row[0], row[1], row[2], row[3], row[4], row[5]) s.token = row[6] s.sendAttemptNumber = row[7] return s sid = self.addValSession(medium, address, clientSecret, time_msec(), commit=False) tokenString = sydent.util.tokenutils.generateNumericTokenOfLength( int(self.sydent.cfg.get('email', 'token.length'))) cur.execute("insert into threepid_token_auths (validationSession, token, sendAttemptNumber) values (?, ?, ?)", (sid, tokenString, -1)) self.sydent.db.commit() s = ValidationSession(sid, medium, address, clientSecret, False, time_msec()) s.token = tokenString s.sendAttemptNumber = -1 return s
def getMxid(self, medium, address): """ Retrieves the MXID associated with a 3PID. :param medium: The medium of the 3PID. :type medium: unicode :param address: The address of the 3PID. :type address: unicode :return: The associated MXID, or None if no MXID is associated with this 3PID. :rtype: unicode or None """ cur = self.sydent.db.cursor() res = cur.execute( "select mxid from global_threepid_associations where " "medium = ? and lower(address) = lower(?) and notBefore < ? and notAfter > ? " "order by ts desc limit 1", (medium, address, time_msec(), time_msec())) row = res.fetchone() if not row: return None return row[0]
def getOrCreateTokenSession(self, medium, address, clientSecret): cur = self.sydent.db.cursor() cur.execute( "select s.id, s.medium, s.address, s.clientSecret, s.validated, s.mtime, " "t.token, t.sendAttemptNumber from threepid_validation_sessions s,threepid_token_auths t " "where s.medium = ? and s.address = ? and s.clientSecret = ? and t.validationSession = s.id", (medium, address, clientSecret)) row = cur.fetchone() if row: s = ValidationSession(row[0], row[1], row[2], row[3], row[4], row[5]) s.token = row[6] s.sendAttemptNumber = row[7] return s sid = self.addValSession(medium, address, clientSecret, time_msec(), commit=False) tokenString = sydent.util.tokenutils.generateTokenForMedium(medium) cur.execute( "insert into threepid_token_auths (validationSession, token, sendAttemptNumber) values (?, ?, ?)", (sid, tokenString, -1)) self.sydent.db.commit() s = ValidationSession(sid, medium, address, clientSecret, False, time_msec()) s.token = tokenString s.sendAttemptNumber = -1 return s
def retrieveMxidsForHashes(self, addresses: List[str]) -> Dict[str, str]: """Returns a mapping from hash: mxid from a list of given lookup_hash values :param addresses: An array of lookup_hash values to check against the db :returns a dictionary of lookup_hash values to mxids of all discovered matches """ cur = self.sydent.db.cursor() cur.execute("CREATE TEMPORARY TABLE tmp_retrieve_mxids_for_hashes " "(lookup_hash VARCHAR)") cur.execute( "CREATE INDEX tmp_retrieve_mxids_for_hashes_lookup_hash ON " "tmp_retrieve_mxids_for_hashes(lookup_hash)") results = {} try: # Convert list of addresses to list of tuples of addresses tuplized_addresses = [(x, ) for x in addresses] inserted_cap = 0 while inserted_cap < len(tuplized_addresses): cur.executemany( "INSERT INTO tmp_retrieve_mxids_for_hashes(lookup_hash) " "VALUES (?)", tuplized_addresses[inserted_cap:inserted_cap + 500], ) inserted_cap += 500 res = cur.execute( # 'notBefore' is the time the association starts being valid, 'notAfter' the the time at which # it ceases to be valid, so the ts must be greater than 'notBefore' and less than 'notAfter'. "SELECT gta.lookup_hash, gta.mxid FROM global_threepid_associations gta " "JOIN tmp_retrieve_mxids_for_hashes " "ON gta.lookup_hash = tmp_retrieve_mxids_for_hashes.lookup_hash " "WHERE gta.notBefore < ? AND gta.notAfter > ? " "ORDER BY gta.lookup_hash, gta.mxid, gta.ts", (time_msec(), time_msec()), ) # Place the results from the query into a dictionary # Results are sorted from oldest to newest, so if there are multiple mxid's for # the same lookup hash, only the newest mapping will be returned # Type safety: lookup_hash is a nullable string in # global_threepid_associations. But it must be equal to a lookup_hash # in the temporary table thanks to the join condition. # The temporary table gets hashes from the `addresses` argument, # which is a list of (non-None) strings. # So lookup_hash really is a str. lookup_hash: str mxid: str for lookup_hash, mxid in res.fetchall(): results[lookup_hash] = mxid finally: cur.execute("DROP TABLE tmp_retrieve_mxids_for_hashes") return results
def getMxids( self, threepid_tuples: List[Tuple[str, str]]) -> List[Tuple[str, str, str]]: """Given a list of threepid_tuples, return the same list but with mxids appended to each tuple for which a match was found in the database for. Output is ordered by medium, address, timestamp DESC :param threepid_tuples: List containing (medium, address) tuples :return: a list of (medium, address, mxid) tuples """ cur = self.sydent.db.cursor() cur.execute( "CREATE TEMPORARY TABLE tmp_getmxids (medium VARCHAR(16), address VARCHAR(256))" ) cur.execute( "CREATE INDEX tmp_getmxids_medium_lower_address ON tmp_getmxids (medium, lower(address))" ) try: inserted_cap = 0 while inserted_cap < len(threepid_tuples): cur.executemany( "INSERT INTO tmp_getmxids (medium, address) VALUES (?, ?)", threepid_tuples[inserted_cap:inserted_cap + 500], ) inserted_cap += 500 res = cur.execute( # 'notBefore' is the time the association starts being valid, 'notAfter' the the time at which # it ceases to be valid, so the ts must be greater than 'notBefore' and less than 'notAfter'. "SELECT gte.medium, gte.address, gte.ts, gte.mxid FROM global_threepid_associations gte " "JOIN tmp_getmxids ON gte.medium = tmp_getmxids.medium AND lower(gte.address) = lower(tmp_getmxids.address) " "WHERE gte.notBefore < ? AND gte.notAfter > ? " "ORDER BY gte.medium, gte.address, gte.ts DESC", (time_msec(), time_msec()), ) results = [] current = None row: Tuple[str, str, int, str] for row in res.fetchall(): # only use the most recent entry for each # threepid (they're sorted by ts) if (row[0], row[1]) == current: continue current = (row[0], row[1]) results.append((row[0], row[1], row[3])) finally: cur.execute("DROP TABLE tmp_getmxids") return results
def getOrCreateTokenSession( self, medium: str, address: str, clientSecret: str) -> Tuple[ValidationSession, TokenInfo]: """ Retrieves the validation session for a given medium, address and client secret, or creates one if none was found. :param medium: The medium to use when looking up or creating the session. :param address: The address to use when looking up or creating the session. :param clientSecret: The client secret to use when looking up or creating the session. :return: The session that was retrieved or created. """ cur = self.sydent.db.cursor() cur.execute( "select s.id, s.medium, s.address, s.clientSecret, s.validated, s.mtime, " "t.token, t.sendAttemptNumber from threepid_validation_sessions s,threepid_token_auths t " "where s.medium = ? and s.address = ? and s.clientSecret = ? and t.validationSession = s.id", (medium, address, clientSecret), ) row: Optional[Tuple[int, str, str, str, Optional[int], int, str, int]] = cur.fetchone() if row: session = ValidationSession(row[0], row[1], row[2], row[3], bool(row[4]), row[5]) token_info = TokenInfo(row[6], row[7]) return session, token_info sid = self.addValSession(medium, address, clientSecret, time_msec(), commit=False) tokenString = sydent.util.tokenutils.generateTokenForMedium(medium) cur.execute( "insert into threepid_token_auths (validationSession, token, sendAttemptNumber) values (?, ?, ?)", (sid, tokenString, -1), ) self.sydent.db.commit() session = ValidationSession( sid, medium, address, clientSecret, False, time_msec(), ) token_info = TokenInfo(tokenString, -1) return session, token_info
def getMxid(self, medium, address): cur = self.sydent.db.cursor() res = cur.execute("select mxid from global_threepid_associations where " "medium = ? and lower(address) = lower(?) and notBefore < ? and notAfter > ? " "order by ts desc limit 1", (medium, address, time_msec(), time_msec())) row = res.fetchone() if not row: return None return row[0]
def retrieveMxidsForHashes(self, addresses): """Returns a mapping from hash: mxid from a list of given lookup_hash values :param addresses: An array of lookup_hash values to check against the db :type addresses: list[unicode] :returns a dictionary of lookup_hash values to mxids of all discovered matches :rtype: dict[unicode, unicode] """ cur = self.sydent.db.cursor() cur.execute("CREATE TEMPORARY TABLE tmp_retrieve_mxids_for_hashes " "(lookup_hash VARCHAR)") cur.execute( "CREATE INDEX tmp_retrieve_mxids_for_hashes_lookup_hash ON " "tmp_retrieve_mxids_for_hashes(lookup_hash)") results = {} try: # Convert list of addresses to list of tuples of addresses addresses = [(x, ) for x in addresses] inserted_cap = 0 while inserted_cap < len(addresses): cur.executemany( "INSERT INTO tmp_retrieve_mxids_for_hashes(lookup_hash) " "VALUES (?)", addresses[inserted_cap:inserted_cap + 500]) inserted_cap += 500 res = cur.execute( # 'notBefore' is the time the association starts being valid, 'notAfter' the the time at which # it ceases to be valid, so the ts must be greater than 'notBefore' and less than 'notAfter'. "SELECT gta.lookup_hash, gta.mxid FROM global_threepid_associations gta " "JOIN tmp_retrieve_mxids_for_hashes " "ON gta.lookup_hash = tmp_retrieve_mxids_for_hashes.lookup_hash " "WHERE gta.notBefore < ? AND gta.notAfter > ? " "ORDER BY gta.lookup_hash, gta.mxid, gta.ts", (time_msec(), time_msec())) # Place the results from the query into a dictionary # Results are sorted from oldest to newest, so if there are multiple mxid's for # the same lookup hash, only the newest mapping will be returned for lookup_hash, mxid in res.fetchall(): results[lookup_hash] = mxid finally: cur.execute("DROP TABLE tmp_retrieve_mxids_for_hashes") return results
def signedAssociationStringForThreepid(self, medium, address): cur = self.sydent.db.cursor() res = cur.execute("select sgAssoc from global_threepid_associations where " "medium = ? and address = ? and notBefore < ? and notAfter > ? " "order by ts desc limit 1", (medium, address, time_msec(), time_msec())) row = res.fetchone() if not row: return None sgAssocBytes = row[0] return sgAssocBytes
def validateSessionWithToken(sydent, sid, clientSecret, token): """ Attempt to validate a session, identified by the sid, using the token from out-of-band. The client secret is given to prevent attempts to guess the token for a sid. If the session was sucessfully validated, return a dict with 'success': True that can be sent to the client, otherwise return False. """ valSessionStore = ThreePidValSessionStore(sydent) s = valSessionStore.getTokenSessionById(sid) if not s: logger.info("Session ID %s not found", (sid)) return False if not clientSecret == s.clientSecret: logger.info("Incorrect client secret", (sid)) raise IncorrectClientSecretException() if s.mtime + ValidationSession.THREEPID_SESSION_VALIDATION_TIMEOUT_MS < time_msec(): logger.info("Session expired") raise SessionExpiredException() # TODO once we can validate the token oob #if tokenObj.validated and clientSecret == tokenObj.clientSecret: # return True if s.token == token: logger.info("Setting session %s as validated", (s.id)) valSessionStore.setValidated(s.id, True) return {'success': True} else: logger.info("Incorrect token submitted") return False
def sendEmail(sydent, templateName, mailTo, substitutions): mailFrom = sydent.cfg.get('email', 'email.from') mailTemplateFile = sydent.cfg.get('email', templateName) myHostname = os.uname()[1] midRandom = "".join([random.choice(string.ascii_letters) for _ in range(16)]) messageid = "<%d%s@%s>" % (time_msec(), midRandom, myHostname) allSubstitutions = {} allSubstitutions.update(substitutions) allSubstitutions.update({ 'messageid': messageid, 'date': email.utils.formatdate(localtime=False), 'to': mailTo, 'from': mailFrom, }) mailString = open(mailTemplateFile).read() % allSubstitutions rawFrom = email.utils.parseaddr(mailFrom)[1] rawTo = email.utils.parseaddr(mailTo)[1] if rawFrom == '' or rawTo == '': logger.info("Couldn't parse from / to address %s / %s", mailFrom, mailTo) raise EmailAddressException() mailServer = sydent.cfg.get('email', 'email.smtphost') logger.info("Sending mail to %s with mail server: %s" % (mailTo, mailServer,)) try: smtp = smtplib.SMTP(mailServer) smtp.sendmail(rawFrom, rawTo, mailString) smtp.quit() except Exception as origException: twisted.python.log.err() ese = EmailSendException() ese.cause = origException raise ese
def validateSessionWithToken(self, sid, clientSecret, token): valSessionStore = ThreePidValSessionStore(self.sydent) s = valSessionStore.getTokenSessionById(sid) if not s: logger.info("Session ID %s not found", (sid)) return False if not clientSecret == s.clientSecret: logger.info("Incorrect client secret", (sid)) raise IncorrectClientSecretException() if s.mtime + ValidationSession.THREEPID_SESSION_VALIDATION_TIMEOUT_MS < time_msec( ): logger.info("Session expired") raise SessionExpiredException() # TODO once we can validate the token oob #if tokenObj.validated and clientSecret == tokenObj.clientSecret: # return True if s.token == token: logger.info("Setting session %s as validated", (s.id)) valSessionStore.setValidated(s.id, True) return {'success': True} else: logger.info("Incorrect token submitted") return False
def removeAssociation(self, threepid, mxid): cur = self.sydent.db.cursor() # check to see if we have any matching associations first. # We use a REPLACE INTO because we need the resulting row to have # a new ID (such that we know it's a new change that needs to be # replicated) so there's no need to insert a deletion row if there's # nothing to delete. cur.execute( "SELECT COUNT(*) FROM local_threepid_associations " "WHERE medium = ? AND address = ? AND mxid = ?", (threepid['medium'], threepid['address'], mxid) ) row = cur.fetchone() if row[0] > 0: ts = time_msec() cur.execute( "REPLACE INTO local_threepid_associations " "('medium', 'address', 'mxid', 'ts', 'notBefore', 'notAfter') " " values (?, ?, NULL, ?, null, null)", (threepid['medium'], threepid['address'], ts), ) logger.info( "Deleting local assoc for %s/%s/%s replaced %d rows", threepid['medium'], threepid['address'], mxid, cur.rowcount, ) self.sydent.db.commit() else: logger.info( "No local assoc found for %s/%s/%s", threepid['medium'], threepid['address'], mxid, ) raise ValueError("No match found between provided mxid and threepid")
def getValidatedSession(self, sid: int, client_secret: str) -> ValidationSession: """ Retrieve a validated and still-valid session whose client secret matches the one passed in. :param sid: The ID of the session to retrieve. :param client_secret: A client secret to check against the one retrieved from the database. :return: The retrieved session. :raise InvalidSessionIdException: No session could be found with this ID. :raise IncorrectClientSecretException: The session's client secret doesn't match the one passed in. :raise SessionExpiredException: The session exists but has expired. :raise SessionNotValidatedException: The session exists but hasn't been validated yet. """ s = self.getSessionById(sid) if not s: raise InvalidSessionIdException() if not s.client_secret == client_secret: raise IncorrectClientSecretException() if s.mtime + THREEPID_SESSION_VALID_LIFETIME_MS < time_msec(): raise SessionExpiredException() if not s.validated: raise SessionNotValidatedException() return s
def validateSessionWithToken(self, sid, clientSecret, token): valSessionStore = ThreePidValSessionStore(self.sydent) s = valSessionStore.getTokenSessionById(sid) if not s: logger.info("Session ID %s not found", (sid)) return False if not clientSecret == s.clientSecret: logger.info("Incorrect client secret", (sid)) raise IncorrectClientSecretException() if s.mtime + ValidationSession.THREEPID_SESSION_VALIDATION_TIMEOUT < time_msec(): logger.info("Session expired") raise SessionExpiredException() # TODO once we can validate the token oob #if tokenObj.validated and clientSecret == tokenObj.clientSecret: # return True if s.token == token: logger.info("Setting session %s as validated", (s.id)) valSessionStore.setValidated(s.id, True) return {'success': True} else: logger.info("Incorrect token submitted") return False
def requestToken(self, phoneNumber, clientSecret, sendAttempt, nextLink): valSessionStore = ThreePidValSessionStore(self.sydent) msisdn = phonenumbers.format_number( phoneNumber, phonenumbers.PhoneNumberFormat.E164 )[1:] valSession = valSessionStore.getOrCreateTokenSession( medium='msisdn', address=msisdn, clientSecret=clientSecret ) valSessionStore.setMtime(valSession.id, time_msec()) if int(valSession.sendAttemptNumber) >= int(sendAttempt): logger.info("Not texting code because current send attempt (%d) is not less than given send attempt (%s)", int(sendAttempt), int(valSession.sendAttemptNumber)) return valSession.id smsBodyTemplate = self.sydent.cfg.get('sms', 'bodyTemplate') originator = self.getOriginator(phoneNumber) logger.info( "Attempting to text code %s to %s (country %d) with originator %s", valSession.token, msisdn, phoneNumber.country_code, originator ) smsBody = smsBodyTemplate.format(token=valSession.token) self.omSms.sendTextSMS(smsBody, msisdn, originator) valSessionStore.setSendAttemptNumber(valSession.id, sendAttempt) return valSession.id
def signedAssociationStringForThreepid(self, medium, address): cur = self.sydent.db.cursor() res = cur.execute( "select sgAssoc from global_threepid_associations where " "medium = ? and address = ? and notBefore < ? and notAfter > ? " "order by ts desc limit 1", (medium, address, time_msec(), time_msec())) row = res.fetchone() if not row: return None sgAssocBytes = row[0] return sgAssocBytes
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
async def requestToken( self, phoneNumber: phonenumbers.PhoneNumber, clientSecret: str, send_attempt: int, brand: Optional[str] = None, ) -> int: """ Creates or retrieves a validation session and sends an text message to the corresponding phone number address with a token to use to verify the association. :param phoneNumber: The phone number to send the email to. :param clientSecret: The client secret to use. :param send_attempt: The current send attempt. :param brand: A hint at a brand from the request. :return: The ID of the session created (or of the existing one if any) """ if str(phoneNumber.country_code) in self.smsRules: action = self.smsRules[str(phoneNumber.country_code)] if action == "reject": raise DestinationRejectedException() valSessionStore = ThreePidValSessionStore(self.sydent) msisdn = phonenumbers.format_number( phoneNumber, phonenumbers.PhoneNumberFormat.E164)[1:] valSession, token_info = valSessionStore.getOrCreateTokenSession( medium="msisdn", address=msisdn, clientSecret=clientSecret) valSessionStore.setMtime(valSession.id, time_msec()) if token_info.send_attempt_number >= send_attempt: logger.info( "Not texting code because current send attempt (%d) is not less than given send attempt (%s)", send_attempt, token_info.send_attempt_number, ) return valSession.id smsBodyTemplate = self.sydent.config.sms.body_template originator = self.getOriginator(phoneNumber) logger.info( "Attempting to text code %s to %s (country %d) with originator %s", token_info.token, msisdn, phoneNumber.country_code, originator, ) smsBody = smsBodyTemplate.format(token=token_info.token) await self.omSms.sendTextSMS(smsBody, msisdn, originator) valSessionStore.setSendAttemptNumber(valSession.id, send_attempt) return valSession.id
def _pushSucceeded(self, result, peer, maxId): logger.info("Pushed updates up to %d to %s with result %d %s", maxId, peer.servername, result.code, result.phrase) self.peerStore.setLastSentVersionAndPokeSucceeded(peer.servername, maxId, time_msec()) self.pushing = False self.scheduledPush()
def sendEmail(sydent, templateName, mailTo, substitutions): mailFrom = sydent.cfg.get('email', 'email.from') mailTemplateFile = sydent.cfg.get('email', templateName) myHostname = sydent.cfg.get('email', 'email.hostname') if myHostname == '': myHostname = socket.getfqdn() midRandom = "".join( [random.choice(string.ascii_letters) for _ in range(16)]) messageid = "<%d%s@%s>" % (time_msec(), midRandom, myHostname) allSubstitutions = {} allSubstitutions.update(substitutions) allSubstitutions.update({ 'messageid': messageid, 'date': email.utils.formatdate(localtime=False), 'to': mailTo, 'from': mailFrom, }) for k, v in allSubstitutions.items(): allSubstitutions[k] = v.decode('utf8') allSubstitutions[k + "_forhtml"] = cgi.escape(v.decode('utf8')) allSubstitutions[k + "_forurl"] = urllib.quote(v) mailString = open(mailTemplateFile).read() % allSubstitutions rawFrom = email.utils.parseaddr(mailFrom)[1] rawTo = email.utils.parseaddr(mailTo)[1] if rawFrom == '' or rawTo == '': logger.info("Couldn't parse from / to address %s / %s", mailFrom, mailTo) raise EmailAddressException() mailServer = sydent.cfg.get('email', 'email.smtphost') mailPort = sydent.cfg.get('email', 'email.smtpport') mailUsername = sydent.cfg.get('email', 'email.smtpusername') mailPassword = sydent.cfg.get('email', 'email.smtppassword') mailTLSMode = sydent.cfg.get('email', 'email.tlsmode') logger.info("Sending mail to %s with mail server: %s" % ( mailTo, mailServer, )) try: if mailTLSMode == 'SSL' or mailTLSMode == 'TLS': smtp = smtplib.SMTP_SSL(mailServer, mailPort, myHostname) elif mailTLSMode == 'STARTTLS': smtp = smtplib.SMTP(mailServer, mailPort, myHostname) smtp.starttls() else: smtp = smtplib.SMTP(mailServer, mailPort, myHostname) if mailUsername != '': smtp.login(mailUsername, mailPassword) smtp.sendmail(rawFrom, rawTo, mailString.encode('utf-8')) smtp.quit() except Exception as origException: twisted.python.log.err() ese = EmailSendException() ese.cause = origException raise ese
def _pushSucceeded(self, result, peer, maxId): logger.info("Pushed updates up to %d to %s with result %d %s", maxId, peer.servername, result.code, result.phrase) self.peerStore.setLastSentVersionAndPokeSucceeded( peer.servername, maxId, time_msec()) self.pushing = False self.scheduledPush()
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 signedAssociationStringForThreepid(self, medium, address): cur = self.sydent.db.cursor() # We treat address as case-insensitive because that's true for all the threepids # we have currently (we treat the local part of email addresses as case insensitive # which is technically incorrect). If we someday get a case-sensitive threepid, # this can change. res = cur.execute("select sgAssoc from global_threepid_associations where " "medium = ? and lower(address) = lower(?) and notBefore < ? and notAfter > ? " "order by ts desc limit 1", (medium, address, time_msec(), time_msec())) row = res.fetchone() if not row: return None sgAssocBytes = row[0] return sgAssocBytes
def getMxids(self, threepid_tuples): cur = self.sydent.db.cursor() cur.execute( "CREATE TEMPORARY TABLE tmp_getmxids (medium VARCHAR(16), address VARCHAR(256))" ) cur.execute( "CREATE INDEX tmp_getmxids_medium_lower_address ON tmp_getmxids (medium, lower(address))" ) try: inserted_cap = 0 while inserted_cap < len(threepid_tuples): cur.executemany( "INSERT INTO tmp_getmxids (medium, address) VALUES (?, ?)", threepid_tuples[inserted_cap:inserted_cap + 500]) inserted_cap += 500 res = cur.execute( # 'notBefore' is the time the association starts being valid, 'notAfter' the the time at which # it ceases to be valid, so the ts must be greater than 'notBefore' and less than 'notAfter'. "SELECT gte.medium, gte.address, gte.ts, gte.mxid FROM global_threepid_associations gte " "JOIN tmp_getmxids ON gte.medium = tmp_getmxids.medium AND lower(gte.address) = lower(tmp_getmxids.address) " "WHERE gte.notBefore < ? AND gte.notAfter > ? " "ORDER BY gte.medium, gte.address, gte.ts DESC", (time_msec(), time_msec())) results = [] current = () for row in res.fetchall(): # only use the most recent entry for each # threepid (they're sorted by ts) if (row[0], row[1]) == current: continue current = (row[0], row[1]) results.append((row[0], row[1], row[3])) finally: res = cur.execute("DROP TABLE tmp_getmxids") return results
def requestToken(self, phoneNumber, clientSecret, sendAttempt, brand=None): """ Creates or retrieves a validation session and sends an text message to the corresponding phone number address with a token to use to verify the association. :param phoneNumber: The phone number to send the email to. :type phoneNumber: phonenumbers.PhoneNumber :param clientSecret: The client secret to use. :type clientSecret: unicode :param sendAttempt: The current send attempt. :type sendAttempt: int :param brand: A hint at a brand from the request. :type brand: str or None :return: The ID of the session created (or of the existing one if any) :rtype: int """ if str(phoneNumber.country_code) in self.smsRules: action = self.smsRules[str(phoneNumber.country_code)] if action == 'reject': raise DestinationRejectedException() valSessionStore = ThreePidValSessionStore(self.sydent) msisdn = phonenumbers.format_number( phoneNumber, phonenumbers.PhoneNumberFormat.E164 )[1:] valSession = valSessionStore.getOrCreateTokenSession( medium='msisdn', address=msisdn, clientSecret=clientSecret ) valSessionStore.setMtime(valSession.id, time_msec()) if int(valSession.sendAttemptNumber) >= int(sendAttempt): logger.info("Not texting code because current send attempt (%d) is not less than given send attempt (%s)", int(sendAttempt), int(valSession.sendAttemptNumber)) return valSession.id smsBodyTemplate = self.sydent.cfg.get('sms', 'bodyTemplate') originator = self.getOriginator(phoneNumber) logger.info( "Attempting to text code %s to %s (country %d) with originator %s", valSession.token, msisdn, phoneNumber.country_code, originator ) smsBody = smsBodyTemplate.format(token=valSession.token) self.omSms.sendTextSMS(smsBody, msisdn, originator) valSessionStore.setSendAttemptNumber(valSession.id, sendAttempt) return valSession.id
async def _push_to_peer(self, p: "RemotePeer") -> None: """ For a given peer, retrieves the list of associations that were created since the last successful push to this peer (limited to ASSOCIATIONS_PUSH_LIMIT) and sends them. :param p: The peer to send associations to. """ logger.debug("Looking for updates to push to %s", p.servername) # Check if a push operation is already active. If so, don't start another if p.is_being_pushed_to: logger.debug( "Waiting for %s to finish pushing...", p.replication_url_origin ) return p.is_being_pushed_to = True try: # Push associations ( assocs, latest_assoc_id, ) = self.local_assoc_store.getSignedAssociationsAfterId( p.lastSentVersion, ASSOCIATIONS_PUSH_LIMIT ) # If there are no updates left to send, break the loop if not assocs: return logger.info( "Pushing %d updates to %s", len(assocs), p.replication_url_origin ) result = await p.pushUpdates(assocs) self.peerStore.setLastSentVersionAndPokeSucceeded( p.servername, latest_assoc_id, time_msec() ) logger.info( "Pushed updates to %s with result %d %s", p.replication_url_origin, result.code, result.phrase, ) except Exception: logger.exception("Error pushing updates to %s", p.replication_url_origin) finally: # Whether pushing completed or an error occurred, signal that pushing has finished p.is_being_pushed_to = False
def getMxids(self, threepid_tuples): cur = self.sydent.db.cursor() cur.execute("CREATE TEMPORARY TABLE tmp_getmxids (medium VARCHAR(16), address VARCHAR(256))"); cur.execute("CREATE INDEX tmp_getmxids_medium_lower_address ON tmp_getmxids (medium, lower(address))"); try: inserted_cap = 0 while inserted_cap < len(threepid_tuples): cur.executemany( "INSERT INTO tmp_getmxids (medium, address) VALUES (?, ?)", threepid_tuples[inserted_cap:inserted_cap + 500] ) inserted_cap += 500 res = cur.execute( # 'notBefore' is the time the association starts being valid, 'notAfter' the the time at which # it ceases to be valid, so the ts must be greater than 'notBefore' and less than 'notAfter'. "SELECT gte.medium, gte.address, gte.ts, gte.mxid FROM global_threepid_associations gte " "JOIN tmp_getmxids ON gte.medium = tmp_getmxids.medium AND lower(gte.address) = lower(tmp_getmxids.address) " "WHERE gte.notBefore < ? AND gte.notAfter > ? " "ORDER BY gte.medium, gte.address, gte.ts DESC", (time_msec(), time_msec()) ) results = [] current = () for row in res.fetchall(): # only use the most recent entry for each # threepid (they're sorted by ts) if (row[0], row[1]) == current: continue current = (row[0], row[1]) results.append((row[0], row[1], row[3])) finally: res = cur.execute("DROP TABLE tmp_getmxids") return results
def validateSessionWithToken(sydent, sid, clientSecret, token): """ Attempt to validate a session, identified by the sid, using the token from out-of-band. The client secret is given to prevent attempts to guess the token for a sid. :param sid: The ID of the session to validate. :type sid: unicode :param clientSecret: The client secret to validate. :type clientSecret: unicode :param token: The token to validate. :type token: unicode :return: A dict with a "success" key which is True if the session was successfully validated, False otherwise. :rtype: dict[str, bool] :raise IncorrectClientSecretException: The provided client_secret is incorrect. :raise SessionExpiredException: The session has expired. :raise InvalidSessionIdException: The session ID couldn't be matched with an existing session. :raise IncorrectSessionTokenException: The provided token is incorrect """ valSessionStore = ThreePidValSessionStore(sydent) s = valSessionStore.getTokenSessionById(sid) if not s: logger.info("Session ID %s not found", sid) raise InvalidSessionIdException() if not clientSecret == s.clientSecret: logger.info("Incorrect client secret", sid) raise IncorrectClientSecretException() if s.mtime + ValidationSession.THREEPID_SESSION_VALIDATION_TIMEOUT_MS < time_msec( ): logger.info("Session expired") raise SessionExpiredException() # TODO once we can validate the token oob #if tokenObj.validated and clientSecret == tokenObj.clientSecret: # return True if s.token == token: logger.info("Setting session %s as validated", s.id) valSessionStore.setValidated(s.id, True) return {'success': True} else: logger.info("Incorrect token submitted") raise IncorrectSessionTokenException()
def removeAssociation(self, threepid: Dict[str, str], mxid: str) -> None: """ Delete the association between a 3PID and a MXID, if it exists. If the association doesn't exist, log and do nothing. Please note that email addresses must be casefolded before calling this function. :param threepid: The 3PID of the binding to remove. :param mxid: The MXID of the binding to remove. """ cur = self.sydent.db.cursor() # check to see if we have any matching associations first. # We use a REPLACE INTO because we need the resulting row to have # a new ID (such that we know it's a new change that needs to be # replicated) so there's no need to insert a deletion row if there's # nothing to delete. cur.execute( "SELECT COUNT(*) FROM local_threepid_associations " "WHERE medium = ? AND address = ? AND mxid = ?", (threepid["medium"], threepid["address"], mxid), ) row: Tuple[int] = cur.fetchone() if row[0] > 0: ts = time_msec() cur.execute( "REPLACE INTO local_threepid_associations " "('medium', 'address', 'mxid', 'ts', 'notBefore', 'notAfter') " " values (?, ?, NULL, ?, null, null)", (threepid["medium"], threepid["address"], ts), ) logger.info( "Deleting local assoc for %s/%s/%s replaced %d rows", threepid["medium"], threepid["address"], mxid, cur.rowcount, ) self.sydent.db.commit() else: logger.info( "No local assoc found for %s/%s/%s", threepid["medium"], threepid["address"], mxid, )
def addBinding(self, medium, address, mxid): """Binds the given 3pid to the given mxid. It's assumed that we have somehow validated that the given user owns the given 3pid Args: medium (str): the type of 3pid address (str): the 3pid mxid (str): the mxid to bind it to """ localAssocStore = LocalAssociationStore(self.sydent) createdAt = time_msec() expires = createdAt + ThreepidBinder.THREEPID_ASSOCIATION_LIFETIME_MS assoc = ThreepidAssociation(medium, address, mxid, createdAt, createdAt, expires) localAssocStore.addOrUpdateAssociation(assoc) self.sydent.pusher.doLocalPush() joinTokenStore = JoinTokenStore(self.sydent) pendingJoinTokens = joinTokenStore.getTokens(medium, address) invites = [] for token in pendingJoinTokens: token["mxid"] = mxid token["signed"] = { "mxid": mxid, "token": token["token"], } token["signed"] = signedjson.sign.sign_json( token["signed"], self.sydent.server_name, self.sydent.keyring.ed25519) invites.append(token) if invites: assoc.extra_fields["invites"] = invites joinTokenStore.markTokensAsSent(medium, address) signer = Signer(self.sydent) sgassoc = signer.signedThreePidAssociation(assoc) self._notify(sgassoc, 0) return sgassoc
def addBinding(self, valSessionId, clientSecret, mxid): valSessionStore = ThreePidValSessionStore(self.sydent) localAssocStore = LocalAssociationStore(self.sydent) s = valSessionStore.getValidatedSession(valSessionId, clientSecret) createdAt = time_msec() expires = createdAt + ThreepidBinder.THREEPID_ASSOCIATION_LIFETIME_MS assoc = ThreepidAssociation(s.medium, s.address, mxid, createdAt, createdAt, expires) localAssocStore.addOrUpdateAssociation(assoc) self.sydent.pusher.doLocalPush() assocSigner = AssociationSigner(self.sydent) sgassoc = assocSigner.signedThreePidAssociation(assoc) return sgassoc
def getValidatedSession(self, sid, clientSecret): """ Retrieve a validated and still-valid session whose client secret matches the one passed in """ s = self.getSessionById(sid) if not s: raise InvalidSessionIdException() if not s.clientSecret == clientSecret: raise IncorrectClientSecretException() if s.mtime + ValidationSession.THREEPID_SESSION_VALID_LIFETIME_MS < time_msec(): raise SessionExpiredException() if not s.validated: raise SessionNotValidatedException() return s
def addBinding(self, medium, address, mxid): """Binds the given 3pid to the given mxid. It's assumed that we have somehow validated that the given user owns the given 3pid Args: medium (str): the type of 3pid address (str): the 3pid mxid (str): the mxid to bind it to """ localAssocStore = LocalAssociationStore(self.sydent) createdAt = time_msec() expires = createdAt + ThreepidBinder.THREEPID_ASSOCIATION_LIFETIME_MS assoc = ThreepidAssociation(medium, address, mxid, createdAt, createdAt, expires) localAssocStore.addOrUpdateAssociation(assoc) self.sydent.pusher.doLocalPush() joinTokenStore = JoinTokenStore(self.sydent) pendingJoinTokens = joinTokenStore.getTokens(medium, address) invites = [] for token in pendingJoinTokens: token["mxid"] = mxid token["signed"] = { "mxid": mxid, "token": token["token"], } token["signed"] = signedjson.sign.sign_json(token["signed"], self.sydent.server_name, self.sydent.keyring.ed25519) invites.append(token) if invites: assoc.extra_fields["invites"] = invites joinTokenStore.markTokensAsSent(medium, address) signer = Signer(self.sydent) sgassoc = signer.signedThreePidAssociation(assoc) self._notify(sgassoc, 0) return sgassoc
def getValidatedSession(self, sid, clientSecret): """ Retrieve a validated and still-valid session whose client secret matches the one passed in """ s = self.getSessionById(sid) if not s: raise InvalidSessionIdException() if not s.clientSecret == clientSecret: raise IncorrectClientSecretException() if s.mtime + ValidationSession.THREEPID_SESSION_VALID_LIFETIME_MS < time_msec( ): raise SessionExpiredException() if not s.validated: raise SessionNotValidatedException() return s
def deleteOldSessions(self) -> None: """Delete old threepid validation sessions that are long expired.""" cur = self.sydent.db.cursor() delete_before_ts = time_msec() - 5 * THREEPID_SESSION_VALID_LIFETIME_MS sql = """ DELETE FROM threepid_validation_sessions WHERE mtime < ? """ cur.execute(sql, (delete_before_ts, )) sql = """ DELETE FROM threepid_token_auths WHERE validationSession NOT IN ( SELECT id FROM threepid_validation_sessions ) """ cur.execute(sql) self.sydent.db.commit()
def addBinding(self, valSessionId, clientSecret, mxid): valSessionStore = ThreePidValSessionStore(self.sydent) localAssocStore = LocalAssociationStore(self.sydent) s = valSessionStore.getValidatedSession(valSessionId, clientSecret) createdAt = time_msec() expires = createdAt + ThreepidBinder.THREEPID_ASSOCIATION_LIFETIME_MS assoc = ThreepidAssociation(s.medium, s.address, mxid, createdAt, createdAt, expires) localAssocStore.addOrUpdateAssociation(assoc) self.sydent.pusher.doLocalPush() joinTokenStore = JoinTokenStore(self.sydent) pendingJoinTokens = joinTokenStore.getTokens(s.medium, s.address) invites = [] for token in pendingJoinTokens: token["mxid"] = mxid token["signed"] = { "mxid": mxid, "token": token["token"], } token["signed"] = signedjson.sign.sign_json( token["signed"], self.sydent.server_name, self.sydent.keyring.ed25519) invites.append(token) if invites: assoc.extra_fields["invites"] = invites joinTokenStore.markTokensAsSent(s.medium, s.address) assocSigner = AssociationSigner(self.sydent) sgassoc = assocSigner.signedThreePidAssociation(assoc) self._notify(sgassoc, 0) return sgassoc
def sendEmail(sydent, templateName, mailTo, substitutions): mailFrom = sydent.cfg.get('email', 'email.from') mailTemplateFile = sydent.cfg.get('email', templateName) myHostname = os.uname()[1] midRandom = "".join( [random.choice(string.ascii_letters) for _ in range(16)]) messageid = "<%d%s@%s>" % (time_msec(), midRandom, myHostname) allSubstitutions = {} allSubstitutions.update(substitutions) allSubstitutions.update({ 'messageid': messageid, 'date': email.utils.formatdate(localtime=False), 'to': mailTo, 'from': mailFrom, }) mailString = open(mailTemplateFile).read() % allSubstitutions rawFrom = email.utils.parseaddr(mailFrom)[1] rawTo = email.utils.parseaddr(mailTo)[1] if rawFrom == '' or rawTo == '': logger.info("Couldn't parse from / to address %s / %s", mailFrom, mailTo) raise EmailAddressException() mailServer = sydent.cfg.get('email', 'email.smtphost') logger.info("Sending mail to %s with mail server: %s" % ( mailTo, mailServer, )) try: smtp = smtplib.SMTP(mailServer) smtp.sendmail(rawFrom, rawTo, mailString) smtp.quit() except Exception as origException: twisted.python.log.err() ese = EmailSendException() ese.cause = origException raise ese
def deleteOldSessions(self): """Delete old threepid validation sessions that are long expired. """ cur = self.sydent.db.cursor() delete_before_ts = time_msec() - 5 * ValidationSession.THREEPID_SESSION_VALID_LIFETIME_MS sql = """ DELETE FROM threepid_validation_sessions WHERE mtime < ? """ cur.execute(sql, (delete_before_ts,)) sql = """ DELETE FROM threepid_token_auths WHERE validationSession NOT IN ( SELECT id FROM threepid_validation_sessions ) """ cur.execute(sql) self.sydent.db.commit()
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 requestToken(self, phoneNumber, clientSecret, sendAttempt, nextLink): if str(phoneNumber.country_code) in self.smsRules: action = self.smsRules[str(phoneNumber.country_code)] if action == 'reject': raise DestinationRejectedException() valSessionStore = ThreePidValSessionStore(self.sydent) msisdn = phonenumbers.format_number( phoneNumber, phonenumbers.PhoneNumberFormat.E164 )[1:] valSession = valSessionStore.getOrCreateTokenSession( medium='msisdn', address=msisdn, clientSecret=clientSecret ) valSessionStore.setMtime(valSession.id, time_msec()) if int(valSession.sendAttemptNumber) >= int(sendAttempt): logger.info("Not texting code because current send attempt (%d) is not less than given send attempt (%s)", int(sendAttempt), int(valSession.sendAttemptNumber)) return valSession.id smsBodyTemplate = self.sydent.cfg.get('sms', 'bodyTemplate') originator = self.getOriginator(phoneNumber) logger.info( "Attempting to text code %s to %s (country %d) with originator %s", valSession.token, msisdn, phoneNumber.country_code, originator ) smsBody = smsBodyTemplate.format(token=valSession.token) self.omSms.sendTextSMS(smsBody, msisdn, originator) valSessionStore.setSendAttemptNumber(valSession.id, sendAttempt) return valSession.id
def addBinding(self, valSessionId, clientSecret, mxid): valSessionStore = ThreePidValSessionStore(self.sydent) localAssocStore = LocalAssociationStore(self.sydent) s = valSessionStore.getValidatedSession(valSessionId, clientSecret) createdAt = time_msec() expires = createdAt + ThreepidBinder.THREEPID_ASSOCIATION_LIFETIME_MS assoc = ThreepidAssociation(s.medium, s.address, mxid, createdAt, createdAt, expires) localAssocStore.addOrUpdateAssociation(assoc) self.sydent.pusher.doLocalPush() joinTokenStore = JoinTokenStore(self.sydent) pendingJoinTokens = joinTokenStore.getTokens(s.medium, s.address) invites = [] for token in pendingJoinTokens: token["mxid"] = mxid token["signed"] = { "mxid": mxid, "token": token["token"], } token["signed"] = signedjson.sign.sign_json(token["signed"], self.sydent.server_name, self.sydent.keyring.ed25519) invites.append(token) if invites: assoc.extra_fields["invites"] = invites joinTokenStore.markTokensAsSent(s.medium, s.address) assocSigner = AssociationSigner(self.sydent) sgassoc = assocSigner.signedThreePidAssociation(assoc) self._notify(sgassoc, 0) return sgassoc
def sendEmail(sydent, templateName, mailTo, substitutions): mailFrom = sydent.cfg.get('email', 'email.from') mailTemplateFile = sydent.cfg.get('email', templateName) myHostname = sydent.cfg.get('email', 'email.hostname') if myHostname == '': myHostname = socket.getfqdn() midRandom = "".join([random.choice(string.ascii_letters) for _ in range(16)]) messageid = "<%d%s@%s>" % (time_msec(), midRandom, myHostname) allSubstitutions = {} allSubstitutions.update(substitutions) allSubstitutions.update({ 'messageid': messageid, 'date': email.utils.formatdate(localtime=False), 'to': mailTo, 'from': mailFrom, }) for k,v in allSubstitutions.items(): allSubstitutions[k] = v.decode('utf8') allSubstitutions[k+"_forhtml"] = cgi.escape(v.decode('utf8')) allSubstitutions[k+"_forurl"] = urllib.quote(v) mailString = open(mailTemplateFile).read().decode('utf8') % allSubstitutions parsedFrom = email.utils.parseaddr(mailFrom)[1] parsedTo = email.utils.parseaddr(mailTo)[1] if parsedFrom == '' or parsedTo == '': logger.info("Couldn't parse from / to address %s / %s", mailFrom, mailTo) raise EmailAddressException() if parsedTo != mailTo: logger.info("Parsed to address changed the address: %s -> %s", mailTo, parsedTo) raise EmailAddressException() mailServer = sydent.cfg.get('email', 'email.smtphost') mailPort = sydent.cfg.get('email', 'email.smtpport') mailUsername = sydent.cfg.get('email', 'email.smtpusername') mailPassword = sydent.cfg.get('email', 'email.smtppassword') mailTLSMode = sydent.cfg.get('email', 'email.tlsmode') logger.info("Sending mail to %s with mail server: %s" % (mailTo, mailServer,)) try: if mailTLSMode == 'SSL' or mailTLSMode == 'TLS': smtp = smtplib.SMTP_SSL(mailServer, mailPort, myHostname) elif mailTLSMode == 'STARTTLS': smtp = smtplib.SMTP(mailServer, mailPort, myHostname) smtp.starttls() else: smtp = smtplib.SMTP(mailServer, mailPort, myHostname) if mailUsername != '': smtp.login(mailUsername, mailPassword) # We're using the parsing above to do basic validation, but instead of # failing it may munge the address it returns. So we should *not* use # that parsed address, as it may not match any validation done # elsewhere. smtp.sendmail(mailFrom, mailTo, mailString.encode('utf-8')) smtp.quit() except Exception as origException: twisted.python.log.err() ese = EmailSendException() ese.cause = origException raise ese