def validateTo(self, user): self.messageid = smtp.messageid().split('@')[0].strip('<') try: rules.validateTo(self, user) except: raise else: msg = { 'from': [user.orig.local, user.orig.domain], 'rcpt': [user.dest.local, user.dest.domain], 'transaction_id': self.messageid, 'id': smtp.messageid().split('@')[0].strip('<') } return lambda: SmtpMessage(msg)
def test_postModerated(self): """ L{INewsStorage.postRequest} forwards a message to the moderator if it does not include an I{Approved} header. """ group = "example.group" moderator = "*****@*****.**" mailhost = "127.0.0.1" sender = "*****@*****.**" articleID = messageid() storage = self.getStorage( [group], {group: [moderator]}, mailhost, sender) message = self.getUnapprovedMessage(articleID, group) result = storage.postRequest(message) def cbModerated(ignored): self.assertEqual(len(self._email), 1) self.assertEqual(self._email[0][0], mailhost) self.assertEqual(self._email[0][1], sender) self.assertEqual(self._email[0][2], [moderator]) self._checkModeratorMessage( self._email[0][3], sender, moderator, group, message) self.assertEqual(self._email[0][4], None) self.assertEqual(self._email[0][5], 25) exists = storage.articleExistsRequest(articleID) exists.addCallback(self.assertFalse) return exists result.addCallback(cbModerated) return result
def generateBounce(message, failedFrom, failedTo, transcript=''): if not transcript: transcript = '''\ I'm sorry, the following address has permanent errors: %(failedTo)s. I've given up, and I will not retry the message again. ''' % vars() boundary = "%s_%s_%s" % (time.time(), os.getpid(), 'XXXXX') failedAddress = rfc822.AddressList(failedTo)[0][1] failedDomain = failedAddress.split('@', 1)[1] messageID = smtp.messageid(uniq='bounce') ctime = time.ctime(time.time()) fp = StringIO.StringIO() fp.write(BOUNCE_FORMAT % vars()) orig = message.tell() message.seek(2, 0) sz = message.tell() message.seek(0, orig) if sz > 10000: while 1: line = message.readline() if len(line) <= 1: break fp.write(line) else: fp.write(message.read()) return '', failedFrom, fp.getvalue()
def test_postModerated(self): """ L{INewsStorage.postRequest} forwards a message to the moderator if it does not include an I{Approved} header. """ group = "example.group" moderator = "*****@*****.**" mailhost = "127.0.0.1" sender = "*****@*****.**" articleID = messageid() storage = self.getStorage( [group], {group: [moderator]}, mailhost, sender) message = self.getUnapprovedMessage(articleID, group) result = storage.postRequest(message) def cbModerated(ignored): self.assertEquals(len(self._email), 1) self.assertEquals(self._email[0][0], mailhost) self.assertEquals(self._email[0][1], sender) self.assertEquals(self._email[0][2], [moderator]) self._checkModeratorMessage( self._email[0][3], sender, moderator, group, message) self.assertEquals(self._email[0][4], None) self.assertEquals(self._email[0][5], 25) exists = storage.articleExistsRequest(articleID) exists.addCallback(self.assertFalse) return exists result.addCallback(cbModerated) return result
def generateBounce(message, failedFrom, failedTo, transcript=''): if not transcript: transcript = '''\ I'm sorry, the following address has permanent errors: %(failedTo)s. I've given up, and I will not retry the message again. ''' % vars() boundary = "%s_%s_%s" % (time.time(), os.getpid(), 'XXXXX') failedAddress = rfc822.AddressList(failedTo)[0][1] failedDomain = failedAddress.split('@', 1)[1] messageID = smtp.messageid(uniq='bounce') ctime = time.ctime(time.time()) fp = StringIO.StringIO() fp.write(BOUNCE_FORMAT % vars()) orig = message.tell() message.seek(2, 0) sz = message.tell() message.seek(0, orig) if sz > 10000: while 1: line = message.readline() if len(line)<=1: break fp.write(line) else: fp.write(message.read()) return '', failedFrom, fp.getvalue()
def compose_plain_email(fromEmail, toEmail, content, headers): msg = MIMEText(content) # Setup the mail headers for (header, value) in headers.items(): msg[header] = value msg.set_charset('utf-8') headkeys = [k.lower() for k in headers.keys()] # Add required headers if not present if "message-id" not in headkeys: msg["Message-ID"] = messageid() if "date" not in headkeys: msg["Date"] = rfc822date() if "from" not in headkeys and "sender" not in headkeys: msg["From"] = fromEmail if "to" not in headkeys and "cc" not in headkeys and "bcc" not in headkeys: msg["To"] = toEmail if "reply-to" not in headkeys: msg["Reply-To"] = SUPPORT_ADDRESS if "user-agent" not in headkeys: msg["User-Agent"] = USER_AGENT return msg.as_string()
def sendEmail(self, url, attempt, email, _sendEmail=_sendEmail): """ Send an email for the given L{_PasswordResetAttempt}. @type url: L{URL} @param url: The URL of the password reset page. @type attempt: L{_PasswordResetAttempt} @param attempt: An L{Item} representing a particular user's attempt to reset their password. @type email: C{str} @param email: The email will be sent to this address. """ host = url.netloc.split(':', 1)[0] from_ = 'reset@' + host body = file(sibpath(__file__, 'reset.rfc2822')).read() body %= { 'from': from_, 'to': email, 'date': rfc822.formatdate(), 'message-id': smtp.messageid(), 'link': url.child(attempt.key) } _sendEmail(from_, email, body)
def send_plain_email(smtphost, username, password, fromEmail, toEmail, content, headers, senderDomainName=None, port=25, requireSSL=True): requireAuth = bool(password) msg = MIMEText(content) # Setup the mail headers for (header, value) in headers.items(): msg[header] = value headkeys = [k.lower() for k in headers.keys()] # Add required headers if not present if "message-id" not in headkeys: msg["Message-ID"] = messageid() if "date" not in headkeys: msg["Date"] = rfc822date() if "from" not in headkeys and "sender" not in headkeys: msg["From"] = fromEmail if "to" not in headkeys and "cc" not in headkeys and "bcc" not in headkeys: msg["To"] = toEmail # send message f = StringIO(msg.as_string()) d = defer.Deferred() factory = ESMTPSenderFactory(username, password, fromEmail, toEmail, f, d, requireAuthentication=requireAuth, requireTransportSecurity=requireSSL) if senderDomainName is not None: factory.domain = senderDomainName connectTCP(smtphost, port, factory) return d
def compose_plain_email(fromEmail, toEmail, content, headers): msg = MIMEText(content) # Setup the mail headers for (header, value) in headers.items(): msg[header] = value msg.set_charset('utf-8') headkeys = [k.lower() for k in headers.keys()] # Add required headers if not present if "message-id" not in headkeys: msg["Message-ID"] = messageid() if "date" not in headkeys: msg["Date"] = rfc822date() if "from" not in headkeys and "sender" not in headkeys: msg["From"] = fromEmail if "to" not in headkeys and "cc" not in headkeys and "bcc" not in headkeys: msg["To"] = toEmail if "reply-to" not in headkeys: msg["Reply-To"] = SUPPORT_ADDRESS if "user-agent" not in headkeys: msg["User-Agent"] = USER_AGENT return msg.as_string()
def sendEmail(self, url, attempt, email, _sendEmail=_sendEmail): """ Send an email for the given L{_PasswordResetAttempt}. @type url: L{URL} @param url: The URL of the password reset page. @type attempt: L{_PasswordResetAttempt} @param attempt: An L{Item} representing a particular user's attempt to reset their password. @type email: C{str} @param email: The email will be sent to this address. """ host = url.netloc.split(':', 1)[0] from_ = 'reset@' + host body = file(sibpath(__file__, 'reset.rfc2822')).read() body %= {'from': from_, 'to': email, 'date': rfc822.formatdate(), 'message-id': smtp.messageid(), 'link': url.child(attempt.key)} _sendEmail(from_, email, body)
def insertResentHeaders(i): m._headers.insert(i, ('resent-from', MH.Header( fromAddress.address).encode())) m._headers.insert(i, ('resent-to', MH.Header( mimeutil.flattenEmailAddresses(toAddresses)).encode())) m._headers.insert(i, ('resent-date', EU.formatdate())) m._headers.insert(i, ('resent-message-id', smtp.messageid('divmod.xquotient')))
def _fix_headers(self, origmsg, newmsg, signkey): """ Move some headers from C{origmsg} to C{newmsg}, delete unwanted headers from C{origmsg} and add new headers to C{newms}. Outgoing messages are either encrypted and signed or just signed before being sent. Because of that, they are packed inside new messages and some manipulation has to be made on their headers. Allowed headers for passing through: - From - Date - To - Subject - Reply-To - References - In-Reply-To - Cc Headers to be added: - Message-ID (i.e. should not use origmsg's Message-Id) - Received (this is added automatically by twisted smtp API) - OpenPGP (see #4447) Headers to be deleted: - User-Agent :param origmsg: The original message. :type origmsg: email.message.Message :param newmsg: The new message being created. :type newmsg: email.message.Message :param signkey: The key used to sign C{newmsg} :type signkey: OpenPGPKey """ # move headers from origmsg to newmsg headers = origmsg.items() passthrough = [ 'from', 'date', 'to', 'subject', 'reply-to', 'references', 'in-reply-to', 'cc' ] headers = filter(lambda x: x[0].lower() in passthrough, headers) for hkey, hval in headers: newmsg.add_header(hkey, hval) del(origmsg[hkey]) # add a new message-id to newmsg newmsg.add_header('Message-Id', smtp.messageid()) # add openpgp header to newmsg username, domain = signkey.address.split('@') newmsg.add_header( 'OpenPGP', 'id=%s' % signkey.key_id, url='https://%s/key/%s' % (domain, username), preference='signencrypt') # delete user-agent from origmsg del(origmsg['user-agent'])
def _fix_headers(self, origmsg, newmsg, signkey): """ Move some headers from C{origmsg} to C{newmsg}, delete unwanted headers from C{origmsg} and add new headers to C{newms}. Outgoing messages are either encrypted and signed or just signed before being sent. Because of that, they are packed inside new messages and some manipulation has to be made on their headers. Allowed headers for passing through: - From - Date - To - Subject - Reply-To - References - In-Reply-To - Cc Headers to be added: - Message-ID (i.e. should not use origmsg's Message-Id) - Received (this is added automatically by twisted smtp API) - OpenPGP (see #4447) Headers to be deleted: - User-Agent :param origmsg: The original message. :type origmsg: email.message.Message :param newmsg: The new message being created. :type newmsg: email.message.Message :param signkey: The key used to sign C{newmsg} :type signkey: OpenPGPKey """ # move headers from origmsg to newmsg headers = origmsg.items() passthrough = [ 'from', 'date', 'to', 'subject', 'reply-to', 'references', 'in-reply-to', 'cc' ] headers = filter(lambda x: x[0].lower() in passthrough, headers) for hkey, hval in headers: newmsg.add_header(hkey, hval) del (origmsg[hkey]) # add a new message-id to newmsg newmsg.add_header('Message-Id', smtp.messageid()) # add openpgp header to newmsg username, domain = signkey.address.split('@') newmsg.add_header('OpenPGP', 'id=%s' % signkey.key_id, url='https://%s/key/%s' % (domain, username), preference='signencrypt') # delete user-agent from origmsg del (origmsg['user-agent'])
def issueViaEmail(self, issuer, email, product, templateData, domainName, httpPort=80): """ Send a ticket via email to the supplied address, which, when claimed, will create an avatar and allow the given product to endow it with things. @param issuer: An object, preferably a user, to track who issued this ticket. @param email: a str, formatted as an rfc2821 email address (user@domain) -- source routes not allowed. @param product: an instance of L{Product} @param domainName: a domain name, used as the domain part of the sender's address, and as the web server to generate a link to within the email. @param httpPort: a port number for the web server running on domainName @param templateData: A string containing an rfc2822-format email message, which will have several python values interpolated into it dictwise: %(from)s: To be used for the From: header; will contain an rfc2822-format address. %(to)s: the address that we are going to send to. %(date)s: an rfc2822-format date. %(message-id)s: an rfc2822 message-id %(link)s: an HTTP URL that we are generating a link to. """ ticket = self.createTicket(issuer, unicode(email, 'ascii'), product) nonce = ticket.nonce signupInfo = { 'from': 'signup@' + domainName, 'to': email, 'date': rfc822.formatdate(), 'message-id': smtp.messageid(), 'link': self.ticketLink(domainName, httpPort, nonce) } msg = templateData % signupInfo return ticket, _sendEmail(signupInfo['from'], email, msg)
def betterMessageID(): """ Strip out the domain in the default Twisted Message-ID value and replace with our configured server host name. That will avoid leaking internal app-server host names in a multi-host setup. @return: our safe message-id value @rtype: L{str} """ return "{}@{}>".format(messageid().split("@")[0], config.ServerHostName)
def generateBounce(message, failedFrom, failedTo, transcript=''): """ Generate a bounce message for an undeliverable email message. @type message: L{bytes} @param message: The undeliverable message. @type failedFrom: L{bytes} @param failedFrom: The originator of the undeliverable message. @type failedTo: L{bytes} @param failedTo: The destination of the undeliverable message. @type transcript: L{bytes} @param transcript: An error message to include in the bounce message. @rtype: 3-L{tuple} of (E{1}) L{bytes}, (E{2}) L{bytes}, (E{3}) L{bytes} @return: The originator, the destination and the contents of the bounce message. The destination of the bounce message is the originator of the undeliverable message. """ if not transcript: transcript = '''\ I'm sorry, the following address has permanent errors: %(failedTo)s. I've given up, and I will not retry the message again. ''' % { 'failedTo': failedTo } failedAddress = rfc822.AddressList(failedTo)[0][1] data = { 'boundary': "%s_%s_%s" % (time.time(), os.getpid(), 'XXXXX'), 'ctime': time.ctime(time.time()), 'failedAddress': failedAddress, 'failedDomain': failedAddress.split('@', 1)[1], 'failedFrom': failedFrom, 'failedTo': failedTo, 'messageID': smtp.messageid(uniq='bounce'), 'message': message, 'transcript': transcript, } fp = StringIO.StringIO() fp.write(BOUNCE_FORMAT % data) orig = message.tell() message.seek(2, 0) sz = message.tell() message.seek(0, orig) if sz > 10000: while 1: line = message.readline() if len(line) <= 1: break fp.write(line) else: fp.write(message.read()) return '', failedFrom, fp.getvalue()
def betterMessageID(): """ Strip out the domain in the default Twisted Message-ID value and replace with our configured server host name. That will avoid leaking internal app-server host names in a multi-host setup. @return: our safe message-id value @rtype: L{str} """ return "{}@{}>".format(messageid().split("@")[0], config.ServerHostName)
def generateBounce(message, failedFrom, failedTo, transcript=''): """ Generate a bounce message for an undeliverable email message. @type message: L{bytes} @param message: The undeliverable message. @type failedFrom: L{bytes} @param failedFrom: The originator of the undeliverable message. @type failedTo: L{bytes} @param failedTo: The destination of the undeliverable message. @type transcript: L{bytes} @param transcript: An error message to include in the bounce message. @rtype: 3-L{tuple} of (E{1}) L{bytes}, (E{2}) L{bytes}, (E{3}) L{bytes} @return: The originator, the destination and the contents of the bounce message. The destination of the bounce message is the originator of the undeliverable message. """ if not transcript: transcript = '''\ I'm sorry, the following address has permanent errors: %(failedTo)s. I've given up, and I will not retry the message again. ''' % {'failedTo': failedTo} failedAddress = rfc822.AddressList(failedTo)[0][1] data = { 'boundary': "%s_%s_%s" % (time.time(), os.getpid(), 'XXXXX'), 'ctime': time.ctime(time.time()), 'failedAddress': failedAddress, 'failedDomain': failedAddress.split('@', 1)[1], 'failedFrom': failedFrom, 'failedTo': failedTo, 'messageID': smtp.messageid(uniq='bounce'), 'message': message, 'transcript': transcript, } fp = StringIO.StringIO() fp.write(BOUNCE_FORMAT % data) orig = message.tell() message.seek(2, 0) sz = message.tell() message.seek(0, orig) if sz > 10000: while 1: line = message.readline() if len(line)<=1: break fp.write(line) else: fp.write(message.read()) return '', failedFrom, fp.getvalue()
def writeHeaders(self, fromAddress, toAddress, subject=None, inReplyTo=None, includeMessageID=True, contentType='text/plain; charset="utf-8"', **kwargs): """Write all headers into the response email. :param str fromAddress: The email address for the ``'From:'`` header. :param str toAddress: The email address for the ``'To:'`` header. :type subject: ``None`` or :any:`str` :param subject: The ``'Subject:'`` header. :type inReplyTo: ``None`` or :any:`str` :param inReplyTo: If set, an ``'In-Reply-To:'`` header will be generated. This should be set to the ``'Message-ID:'`` header from the client's original request email. :param bool includeMessageID: If ``True``, generate and include a ``'Message-ID:'`` header for the response. :param str contentType: The ``'Content-Type:'`` header. :kwargs: If given, the key will become the name of the header, and the value will become the contents of that header. """ fromAddress = fromAddress.decode('utf-8') if isinstance( fromAddress, bytes) else fromAddress toAddress = toAddress.decode('utf-8') if isinstance( toAddress, bytes) else toAddress self.write("From: %s" % fromAddress) self.write("To: %s" % toAddress) if includeMessageID: self.write("Message-ID: %s" % smtp.messageid()) if inReplyTo: self.write("In-Reply-To: %s" % inReplyTo) self.write("Content-Type: %s" % contentType) self.write("Date: %s" % smtp.rfc822date().decode('utf-8')) if not subject: subject = '[no subject]' if not subject.lower().startswith('re'): subject = "Re: " + subject self.write("Subject: %s" % subject) if kwargs: for headerName, headerValue in kwargs.items(): headerName = headerName.capitalize() headerName = headerName.replace(' ', '-') headerName = headerName.replace('_', '-') header = "%s: %s" % (headerName, headerValue) self.write(header) # The first blank line designates that the headers have ended: self.write(self.delimiter)
def issueViaEmail(self, issuer, email, product, templateData, domainName, httpPort=80): """ Send a ticket via email to the supplied address, which, when claimed, will create an avatar and allow the given product to endow it with things. @param issuer: An object, preferably a user, to track who issued this ticket. @param email: a str, formatted as an rfc2821 email address (user@domain) -- source routes not allowed. @param product: an instance of L{Product} @param domainName: a domain name, used as the domain part of the sender's address, and as the web server to generate a link to within the email. @param httpPort: a port number for the web server running on domainName @param templateData: A string containing an rfc2822-format email message, which will have several python values interpolated into it dictwise: %(from)s: To be used for the From: header; will contain an rfc2822-format address. %(to)s: the address that we are going to send to. %(date)s: an rfc2822-format date. %(message-id)s: an rfc2822 message-id %(link)s: an HTTP URL that we are generating a link to. """ ticket = self.createTicket(issuer, unicode(email, 'ascii'), product) nonce = ticket.nonce signupInfo = {'from': 'signup@'+domainName, 'to': email, 'date': rfc822.formatdate(), 'message-id': smtp.messageid(), 'link': self.ticketLink(domainName, httpPort, nonce)} msg = templateData % signupInfo return ticket, _sendEmail(signupInfo['from'], email, msg)
def send_plain_email(fromEmail, toEmail, content, headers): if REQUIRE_AUTH: password = FilePath(SMTP_PASSWORD_PATH).getContent().strip() else: password = None msg = MIMEText(content) # Setup the mail headers for (header, value) in headers.items(): msg[header] = value headkeys = [k.lower() for k in headers.keys()] # Add required headers if not present if "message-id" not in headkeys: msg["Message-ID"] = messageid() if "date" not in headkeys: msg["Date"] = rfc822date() if "from" not in headkeys and "sender" not in headkeys: msg["From"] = fromEmail if "to" not in headkeys and "cc" not in headkeys and "bcc" not in headkeys: msg["To"] = toEmail if "reply-to" not in headkeys: msg["Reply-To"] = SUPPORT_ADDRESS if "user-agent" not in headkeys: msg["User-Agent"] = USER_AGENT if "content-type" not in headkeys: msg["Content-Type"] = CONTENT_TYPE # send message f = StringIO(msg.as_string()) d = defer.Deferred() factory = ESMTPSenderFactory( SMTP_USERNAME, password, fromEmail, toEmail, f, d, requireAuthentication=REQUIRE_AUTH, requireTransportSecurity=REQUIRE_TRANSPORT_SECURITY) factory.domain = SENDER_DOMAIN connectTCP(SMTP_HOST, SMTP_PORT, factory) return d
def writeHeaders(self, fromAddress, toAddress, subject=None, inReplyTo=None, includeMessageID=True, contentType='text/plain; charset="utf-8"', **kwargs): """Write all headers into the response email. :param str fromAddress: The email address for the ``'From:'`` header. :param str toAddress: The email address for the ``'To:'`` header. :type subject: None or str :param subject: The ``'Subject:'`` header. :type inReplyTo: None or str :param inReplyTo: If set, an ``'In-Reply-To:'`` header will be generated. This should be set to the ``'Message-ID:'`` header from the client's original request email. :param bool includeMessageID: If ``True``, generate and include a ``'Message-ID:'`` header for the response. :param str contentType: The ``'Content-Type:'`` header. :kwargs: If given, the key will become the name of the header, and the value will become the Contents of that header. """ self.write("From: %s" % fromAddress) self.write("To: %s" % toAddress) if includeMessageID: self.write("Message-ID: %s" % smtp.messageid().encode('utf-8')) if inReplyTo: self.write("In-Reply-To: %s" % inReplyTo.encode('utf-8')) self.write("Content-Type: %s" % contentType.encode('utf-8')) self.write("Date: %s" % smtp.rfc822date().encode('utf-8')) if not subject: subject = '[no subject]' if not subject.lower().startswith('re'): subject = "Re: " + subject self.write("Subject: %s" % subject.encode('utf-8')) if kwargs: for headerName, headerValue in kwargs.items(): headerName = headerName.capitalize() headerName = headerName.replace(' ', '-') headerName = headerName.replace('_', '-') header = "%s: %s" % (headerName, headerValue) self.write(header.encode('utf-8')) # The first blank line designates that the headers have ended: self.write(self.delimiter)
def notifyModerators(self, moderators, article): """ Send an article to a list of group moderators to be moderated. @param moderators: A C{list} of C{str} giving RFC 2821 addresses of group moderators to notify. @param article: The article requiring moderation. @type article: L{Article} @return: A L{Deferred} which fires with the result of sending the email. """ # Moderated postings go through as long as they have an Approved # header, regardless of what the value is group = article.getHeader('Newsgroups') subject = article.getHeader('Subject') if self._sender is None: # This case should really go away. This isn't a good default. sender = 'twisted-news@' + socket.gethostname() else: sender = self._sender msg = Message() msg['Message-ID'] = smtp.messageid() msg['From'] = sender msg['To'] = ', '.join(moderators) msg['Subject'] = 'Moderate new %s message: %s' % (group, subject) msg['Content-Type'] = 'message/rfc822' payload = Message() for header, value in article.headers.values(): payload.add_header(header, value) payload.set_payload(article.body) msg.attach(payload) out = StringIO.StringIO() gen = Generator(out, False) gen.flatten(msg) msg = out.getvalue() return self.sendmail(self._mailhost, sender, moderators, msg)
def notifyModerators(self, moderators, article): """ Send an article to a list of group moderators to be moderated. @param moderators: A C{list} of C{str} giving RFC 2821 addresses of group moderators to notify. @param article: The article requiring moderation. @type article: L{Article} @return: A L{Deferred} which fires with the result of sending the email. """ # Moderated postings go through as long as they have an Approved # header, regardless of what the value is group = article.getHeader('Newsgroups') subject = article.getHeader('Subject') if self._sender is None: # This case should really go away. This isn't a good default. sender = 'twisted-news@' + socket.gethostname() else: sender = self._sender msg = Message() msg['Message-ID'] = smtp.messageid() msg['From'] = sender msg['To'] = ', '.join(moderators) msg['Subject'] = 'Moderate new %s message: %s' % (group, subject) msg['Content-Type'] = 'message/rfc822' payload = Message() for header, value in article.headers.values(): payload.add_header(header, value) payload.set_payload(article.body) msg.attach(payload) out = StringIO.StringIO() gen = Generator(out, False) gen.flatten(msg) msg = out.getvalue() return self.sendmail(self._mailhost, sender, moderators, msg)
def send_plain_email(fromEmail, toEmail, content, headers): if REQUIRE_AUTH: password = FilePath(SMTP_PASSWORD_PATH).getContent().strip() else: password = None msg = MIMEText(content) # Setup the mail headers for (header, value) in headers.items(): msg[header] = value headkeys = [k.lower() for k in headers.keys()] # Add required headers if not present if "message-id" not in headkeys: msg["Message-ID"] = messageid() if "date" not in headkeys: msg["Date"] = rfc822date() if "from" not in headkeys and "sender" not in headkeys: msg["From"] = fromEmail if "to" not in headkeys and "cc" not in headkeys and "bcc" not in headkeys: msg["To"] = toEmail if "reply-to" not in headkeys: msg["Reply-To"] = SUPPORT_ADDRESS if "user-agent" not in headkeys: msg["User-Agent"] = USER_AGENT if "content-type" not in headkeys: msg["Content-Type"] = CONTENT_TYPE # send message f = StringIO(msg.as_string()) d = defer.Deferred() factory = ESMTPSenderFactory(SMTP_USERNAME, password, fromEmail, toEmail, f, d, requireAuthentication=REQUIRE_AUTH, requireTransportSecurity=REQUIRE_TRANSPORT_SECURITY) factory.domain = SENDER_DOMAIN connectTCP(SMTP_HOST, SMTP_PORT, factory) return d
def test_postApproved(self): """ L{INewsStorage.postRequest} posts the message if it includes an I{Approved} header. """ group = "example.group" moderator = "*****@*****.**" mailhost = "127.0.0.1" sender = "*****@*****.**" articleID = messageid() storage = self.getStorage( [group], {group: [moderator]}, mailhost, sender) message = self.getApprovedMessage(articleID, group) result = storage.postRequest(message) def cbPosted(ignored): self.assertEqual(self._email, []) exists = storage.articleExistsRequest(articleID) exists.addCallback(self.assertTrue) return exists result.addCallback(cbPosted) return result
def test_postApproved(self): """ L{INewsStorage.postRequest} posts the message if it includes an I{Approved} header. """ group = "example.group" moderator = "*****@*****.**" mailhost = "127.0.0.1" sender = "*****@*****.**" articleID = messageid() storage = self.getStorage( [group], {group: [moderator]}, mailhost, sender) message = self.getApprovedMessage(articleID, group) result = storage.postRequest(message) def cbPosted(ignored): self.assertEquals(self._email, []) exists = storage.articleExistsRequest(articleID) exists.addCallback(self.assertTrue) return exists result.addCallback(cbPosted) return result
def MIME_mail_build(src_name, src_mail, dest_name, dest_mail, mail_subject, mail_body): """ Prepare the mail headers :param src_name: A source name :param src_mail: A source email adddress :param dest_name: A destination name :param dest_mail: A destination email address :param mail_subject: A mail subject :param mail_body: A mail body :return: A mail headers """ multipart = MIMEMultipart('alternative') multipart['Message-Id'] = Header(messageid()) multipart['Subject'] = Header(mail_subject.encode(), 'UTF-8').encode() multipart['Date'] = utils.formatdate() multipart['To'] = Header(dest_name.encode(), 'UTF-8').encode() + " <" + dest_mail + ">" multipart['From'] = Header(src_name.encode(), 'UTF-8').encode() + " <" + src_mail + ">" multipart['X-Mailer'] = "fnord" multipart.attach(MIMEText(mail_body.encode(), 'plain', 'UTF-8')) return BytesIO(multipart.as_bytes()) # pylint: disable=no-member
def _format_smtp_message(from_addr, to_addr, subject, content, attachments): # # The central problem in formatting SMTP messages correctly is to support # non-US-ASCII characters correctly. The solution here simply mimics what # Thunderbird (Icedove) does when sending a message containing both ISO 8859-1 # and full Unicode characters. # # The solution for headers and content is unfortunately different. # # # ESMTP: # * http://www.ietf.org/rfc/rfc2821.txt # * http://www.ietf.org/rfc/rfc2822.txt # # MIME (cannot send other an US-ASCII with 2822 format): # * http://en.wikipedia.org/wiki/MIME # * http://en.wikipedia.org/wiki/Unicode_and_e-mail # * http://www.ietf.org/rfc/rfc2045.txt # * http://www.ietf.org/rfc/rfc2046.txt # * http://www.ietf.org/rfc/rfc2048.txt # * http://www.ietf.org/rfc/rfc2049.txt # * http://www.ietf.org/rfc/rfc2183.txt # FIXME: some problems with mixed UTF vs. non-UTF characters in header. # escape all? def _hdrescape(x): res = '' for i in xrange(len(x)): c = x[i] ci = ord(c) if (ci in [0x0a, 0x0d]): # suppress in headers pass elif (ci >= 0x20 and ci <= 0x7e): res += c else: # See: http://en.wikipedia.org/wiki/MIME#Encoded-Word utf = c.encode('utf-8') res += '=?UTF-8?Q?' for j in xrange(len(utf)): res += '=%02X' % ord(utf[j]) res += '?=' return res def _bodyescape(x): res = '' for i in xrange(len(x)): c = x[i] ci = ord(c) if (ci >= 0x20 and ci <= 0x7e) or (ci in [0x0a, 0x0d]): res += c else: utf = c.encode('utf-8') for j in xrange(len(utf)): res += '=%02X' % ord(utf[j]) return res # Check sanity # # No checks are done for attachments at the moment. The attachments are I/O objects # so we don't know their size off hand. However, below, when we're creating a MIME # message, we'll limit the attachment size. if (len(from_addr) > STRING_SANITY) or \ (len(to_addr) > STRING_SANITY) or \ (len(subject) > STRING_SANITY) or \ (len(content) > MAX_CONTENT_SIZE): raise Exception('parameter sanity check failed') # Escape from and to addresses - XXX: no support for non-ascii email addresses right now from_addr = from_addr.encode('ascii') to_addr = to_addr.encode('ascii') # Start building MIME message into a string I/O object fp = cStringIO.StringIO() mw = MimeWriter.MimeWriter(fp) mw.addheader('From', _hdrescape(from_addr)) mw.addheader('To', _hdrescape(to_addr)) mw.addheader('Subject', _hdrescape(subject)) mw.addheader('Date', smtp.rfc822date()) mw.addheader('Message-ID', smtp.messageid()) mw.addheader('MIME-Version', '1.0') # Add body and possibly attachments if len(attachments.keys()) == 0: # XXX: using 'plist' of startbody() results in multiline encoding, which I dislike mw.addheader('Content-Transfer-Encoding', 'quoted-printable') mw.addheader('Content-Disposition', 'inline') mw.flushheaders() f = mw.startbody('text/plain; charset=UTF-8') # XXX: format=flowed? f.write(_bodyescape(content)) else: mw.flushheaders() mw.startmultipartbody('mixed') f = mw.nextpart() f.addheader('Content-Disposition', 'inline') f.addheader('Content-Transfer-Encoding', 'quoted-printable') f.flushheaders() f2 = f.startbody('text/plain; charset=UTF-8') # XXX: format=flowed? f2.write(_bodyescape(content)) for i in attachments.keys(): f = mw.nextpart() f.addheader('Content-Disposition', 'inline; filename=%s' % i) # FIXME: filter / escape filenames f.addheader('Content-Transfer-Encoding', 'base64') f.flushheaders() mimetype, encoding = mimetypes.guess_type(i) f2 = f.startbody(mimetype) fdata = attachments[i] # We read the attachment into a variable here for base64 encoding (and # size check), this may take several megabytes of temporary memory. t = fdata.read(MAX_ATTACHMENT_SIZE + 1) if len(t) > MAX_ATTACHMENT_SIZE: raise Exception('attachment too long') f2.write(t.encode('base64')) mw.lastpart() # Done, convert to string and we're done return fp.getvalue()
def _fix_headers(self, msg, newmsg, sign_address): """ Move some headers from C{origmsg} to C{newmsg}, delete unwanted headers from C{origmsg} and add new headers to C{newms}. Outgoing messages are either encrypted and signed or just signed before being sent. Because of that, they are packed inside new messages and some manipulation has to be made on their headers. Allowed headers for passing through: - From - Date - To - Subject - Reply-To - References - In-Reply-To - Cc Headers to be added: - Message-ID (i.e. should not use origmsg's Message-Id) - Received (this is added automatically by twisted smtp API) - OpenPGP (see #4447) Headers to be deleted: - User-Agent :param msg: The original message. :type msg: email.message.Message :param newmsg: The new message being created. :type newmsg: email.message.Message :param sign_address: The address used to sign C{newmsg} :type sign_address: str :return: A Deferred with a touple: (new Message with the unencrypted headers, original Message with headers removed) :rtype: Deferred """ origmsg = deepcopy(msg) # move headers from origmsg to newmsg headers = origmsg.items() passthrough = [ 'from', 'date', 'to', 'subject', 'reply-to', 'references', 'in-reply-to', 'cc' ] headers = filter(lambda x: x[0].lower() in passthrough, headers) for hkey, hval in headers: newmsg.add_header(hkey, hval) del (origmsg[hkey]) # add a new message-id to newmsg newmsg.add_header('Message-Id', smtp.messageid()) # delete user-agent from origmsg del (origmsg['user-agent']) def add_openpgp_header(signkey): username, domain = sign_address.split('@') newmsg.add_header( 'OpenPGP', 'id=%s' % signkey.fingerprint, url='https://%s/key/%s' % (domain, username), preference='signencrypt') return newmsg, origmsg d = self._keymanager.get_key(sign_address, private=True) d.addCallback(add_openpgp_header) return d
def processReply(self, msg): # extract the token from the To header _ignore_name, addr = email.utils.parseaddr(msg['To']) if addr: # addr looks like: [email protected] token = self._extractToken(addr) if not token: log.error("Mail gateway didn't find a token in message " "%s (%s)" % (msg['Message-ID'], msg['To'])) returnValue(self.NO_TOKEN) else: log.error("Mail gateway couldn't parse To: address (%s) in " "message %s" % (msg['To'], msg['Message-ID'])) returnValue(self.MALFORMED_TO_ADDRESS) txn = self.store.newTransaction(label="MailReceiver.processReply") result = (yield txn.imipLookupByToken(token)) yield txn.commit() try: # Note the results are returned as utf-8 encoded strings organizer, attendee, _ignore_icaluid = result[0] except: # This isn't a token we recognize log.error("Mail gateway found a token (%s) but didn't " "recognize it in message %s" % (token, msg['Message-ID'])) returnValue(self.UNKNOWN_TOKEN) for part in msg.walk(): if part.get_content_type() == "text/calendar": calBody = part.get_payload(decode=True) break else: # No icalendar attachment log.warn("Mail gateway didn't find an icalendar attachment " "in message %s" % (msg['Message-ID'], )) toAddr = None fromAddr = attendee[7:] if organizer.startswith("mailto:"): toAddr = organizer[7:] elif organizer.startswith("urn:x-uid:"): uid = organizer[10:] record = yield self.directory.recordWithUID(uid) try: if record and record.emailAddresses: toAddr = list(record.emailAddresses)[0] except AttributeError: pass if toAddr is None: log.error("Don't have an email address for the organizer; " "ignoring reply.") returnValue(self.NO_ORGANIZER_ADDRESS) settings = config.Scheduling["iMIP"]["Sending"] smtpSender = SMTPSender(settings.Username, settings.Password, settings.UseSSL, settings.Server, settings.Port) del msg["From"] msg["From"] = fromAddr del msg["Reply-To"] msg["Reply-To"] = fromAddr del msg["To"] msg["To"] = toAddr log.warn("Mail gateway forwarding reply back to organizer") yield smtpSender.sendMessage(fromAddr, toAddr, messageid(), msg.as_string()) returnValue(self.REPLY_FORWARDED_TO_ORGANIZER) # Process the imip attachment; inject to calendar server log.debug(calBody) calendar = Component.fromString(calBody) event = calendar.mainComponent() # Don't let a missing PRODID prevent the reply from being processed if not calendar.hasProperty("PRODID"): calendar.addProperty(Property("PRODID", "Unknown")) calendar.removeAllButOneAttendee(attendee) organizerProperty = calendar.getOrganizerProperty() if organizerProperty is None: # ORGANIZER is required per rfc2446 section 3.2.3 log.warn("Mail gateway didn't find an ORGANIZER in REPLY %s" % (msg['Message-ID'], )) event.addProperty(Property("ORGANIZER", organizer)) else: organizerProperty.setValue(organizer) if not calendar.getAttendees(): # The attendee we're expecting isn't there, so add it back # with a SCHEDULE-STATUS of SERVICE_UNAVAILABLE. # The organizer will then see that the reply was not successful. attendeeProp = Property("ATTENDEE", attendee, params={ "SCHEDULE-STATUS": iTIPRequestStatus.SERVICE_UNAVAILABLE, }) event.addProperty(attendeeProp) # TODO: We have talked about sending an email to the reply-to # at this point, to let them know that their reply was missing # the appropriate ATTENDEE. This will require a new localizable # email template for the message. txn = self.store.newTransaction(label="MailReceiver.processReply") yield txn.enqueue(IMIPReplyWork, organizer=organizer, attendee=attendee, icalendarText=str(calendar)) yield txn.commit() returnValue(self.INJECTION_SUBMITTED)
def processReply(self, msg): # extract the token from the To header _ignore_name, addr = email.utils.parseaddr(msg['To']) if addr: # addr looks like: [email protected] token = self._extractToken(addr) if not token: log.error("Mail gateway didn't find a token in message " "%s (%s)" % (msg['Message-ID'], msg['To'])) returnValue(self.NO_TOKEN) else: log.error("Mail gateway couldn't parse To: address (%s) in " "message %s" % (msg['To'], msg['Message-ID'])) returnValue(self.MALFORMED_TO_ADDRESS) txn = self.store.newTransaction() result = (yield txn.imipLookupByToken(token)) yield txn.commit() try: # Note the results are returned as utf-8 encoded strings organizer, attendee, icaluid = result[0] except: # This isn't a token we recognize log.error("Mail gateway found a token (%s) but didn't " "recognize it in message %s" % (token, msg['Message-ID'])) returnValue(self.UNKNOWN_TOKEN) for part in msg.walk(): if part.get_content_type() == "text/calendar": calBody = part.get_payload(decode=True) break else: # No icalendar attachment log.warn("Mail gateway didn't find an icalendar attachment " "in message %s" % (msg['Message-ID'],)) toAddr = None fromAddr = attendee[7:] if organizer.startswith("mailto:"): toAddr = organizer[7:] elif organizer.startswith("urn:uuid:"): guid = organizer[9:] record = self.directory.recordWithGUID(guid) if record and record.emailAddresses: toAddr = list(record.emailAddresses)[0] if toAddr is None: log.error("Don't have an email address for the organizer; " "ignoring reply.") returnValue(self.NO_ORGANIZER_ADDRESS) settings = config.Scheduling["iMIP"]["Sending"] smtpSender = SMTPSender(settings.Username, settings.Password, settings.UseSSL, settings.Server, settings.Port) del msg["From"] msg["From"] = fromAddr del msg["Reply-To"] msg["Reply-To"] = fromAddr del msg["To"] msg["To"] = toAddr log.warn("Mail gateway forwarding reply back to organizer") yield smtpSender.sendMessage(fromAddr, toAddr, messageid(), msg) returnValue(self.REPLY_FORWARDED_TO_ORGANIZER) # Process the imip attachment; inject to calendar server log.debug(calBody) calendar = Component.fromString(calBody) event = calendar.mainComponent() calendar.removeAllButOneAttendee(attendee) organizerProperty = calendar.getOrganizerProperty() if organizerProperty is None: # ORGANIZER is required per rfc2446 section 3.2.3 log.warn("Mail gateway didn't find an ORGANIZER in REPLY %s" % (msg['Message-ID'],)) event.addProperty(Property("ORGANIZER", organizer)) else: organizerProperty.setValue(organizer) if not calendar.getAttendees(): # The attendee we're expecting isn't there, so add it back # with a SCHEDULE-STATUS of SERVICE_UNAVAILABLE. # The organizer will then see that the reply was not successful. attendeeProp = Property("ATTENDEE", attendee, params={ "SCHEDULE-STATUS": iTIPRequestStatus.SERVICE_UNAVAILABLE, } ) event.addProperty(attendeeProp) # TODO: We have talked about sending an email to the reply-to # at this point, to let them know that their reply was missing # the appropriate ATTENDEE. This will require a new localizable # email template for the message. txn = self.store.newTransaction() yield txn.enqueue(IMIPReplyWork, organizer=organizer, attendee=attendee, icalendarText=str(calendar)) yield txn.commit() returnValue(self.INJECTION_SUBMITTED)
def _format_smtp_message(from_addr, to_addr, subject, content, attachments): # # The central problem in formatting SMTP messages correctly is to support # non-US-ASCII characters correctly. The solution here simply mimics what # Thunderbird (Icedove) does when sending a message containing both ISO 8859-1 # and full Unicode characters. # # The solution for headers and content is unfortunately different. # # # ESMTP: # * http://www.ietf.org/rfc/rfc2821.txt # * http://www.ietf.org/rfc/rfc2822.txt # # MIME (cannot send other an US-ASCII with 2822 format): # * http://en.wikipedia.org/wiki/MIME # * http://en.wikipedia.org/wiki/Unicode_and_e-mail # * http://www.ietf.org/rfc/rfc2045.txt # * http://www.ietf.org/rfc/rfc2046.txt # * http://www.ietf.org/rfc/rfc2048.txt # * http://www.ietf.org/rfc/rfc2049.txt # * http://www.ietf.org/rfc/rfc2183.txt # FIXME: some problems with mixed UTF vs. non-UTF characters in header. # escape all? def _hdrescape(x): res = '' for i in xrange(len(x)): c = x[i] ci = ord(c) if (ci in [0x0a, 0x0d]): # suppress in headers pass elif (ci >= 0x20 and ci <= 0x7e): res += c else: # See: http://en.wikipedia.org/wiki/MIME#Encoded-Word utf = c.encode('utf-8') res += '=?UTF-8?Q?' for j in xrange(len(utf)): res += '=%02X' % ord(utf[j]) res += '?=' return res def _bodyescape(x): res = '' for i in xrange(len(x)): c = x[i] ci = ord(c) if (ci >= 0x20 and ci <= 0x7e) or (ci in [0x0a, 0x0d]): res += c else: utf = c.encode('utf-8') for j in xrange(len(utf)): res += '=%02X' % ord(utf[j]) return res # Check sanity # # No checks are done for attachments at the moment. The attachments are I/O objects # so we don't know their size off hand. However, below, when we're creating a MIME # message, we'll limit the attachment size. if (len(from_addr) > STRING_SANITY) or \ (len(to_addr) > STRING_SANITY) or \ (len(subject) > STRING_SANITY) or \ (len(content) > MAX_CONTENT_SIZE): raise Exception('parameter sanity check failed') # Escape from and to addresses - XXX: no support for non-ascii email addresses right now from_addr = from_addr.encode('ascii') to_addr = to_addr.encode('ascii') # Start building MIME message into a string I/O object fp = cStringIO.StringIO() mw = MimeWriter.MimeWriter(fp) mw.addheader('From', _hdrescape(from_addr)) mw.addheader('To', _hdrescape(to_addr)) mw.addheader('Subject', _hdrescape(subject)) mw.addheader('Date', smtp.rfc822date()) mw.addheader('Message-ID', smtp.messageid()) mw.addheader('MIME-Version', '1.0') # Add body and possibly attachments if len(attachments.keys()) == 0: # XXX: using 'plist' of startbody() results in multiline encoding, which I dislike mw.addheader('Content-Transfer-Encoding', 'quoted-printable') mw.addheader('Content-Disposition', 'inline') mw.flushheaders() f = mw.startbody('text/plain; charset=UTF-8') # XXX: format=flowed? f.write(_bodyescape(content)) else: mw.flushheaders() mw.startmultipartbody('mixed') f = mw.nextpart() f.addheader('Content-Disposition', 'inline') f.addheader('Content-Transfer-Encoding', 'quoted-printable') f.flushheaders() f2 = f.startbody('text/plain; charset=UTF-8') # XXX: format=flowed? f2.write(_bodyescape(content)) for i in attachments.keys(): f = mw.nextpart() f.addheader('Content-Disposition', 'inline; filename=%s' % i) # FIXME: filter / escape filenames f.addheader('Content-Transfer-Encoding', 'base64') f.flushheaders() mimetype, encoding = mimetypes.guess_type(i) f2 = f.startbody(mimetype) fdata = attachments[i] # We read the attachment into a variable here for base64 encoding (and # size check), this may take several megabytes of temporary memory. t = fdata.read(MAX_ATTACHMENT_SIZE + 1) if len(t) > MAX_ATTACHMENT_SIZE: raise Exception('attachment too long') f2.write(t.encode('base64')) mw.lastpart() # Done, convert to string and we're done return fp.getvalue()
def generateBounce(message, failedFrom, failedTo, transcript="", encoding="utf-8"): """ Generate a bounce message for an undeliverable email message. @type message: a file-like object @param message: The undeliverable message. @type failedFrom: L{bytes} or L{unicode} @param failedFrom: The originator of the undeliverable message. @type failedTo: L{bytes} or L{unicode} @param failedTo: The destination of the undeliverable message. @type transcript: L{bytes} or L{unicode} @param transcript: An error message to include in the bounce message. @type encoding: L{str} or L{unicode} @param encoding: Encoding to use, default: utf-8 @rtype: 3-L{tuple} of (E{1}) L{bytes}, (E{2}) L{bytes}, (E{3}) L{bytes} @return: The originator, the destination and the contents of the bounce message. The destination of the bounce message is the originator of the undeliverable message. """ if isinstance(failedFrom, bytes): failedFrom = failedFrom.decode(encoding) if isinstance(failedTo, bytes): failedTo = failedTo.decode(encoding) if not transcript: transcript = """\ I'm sorry, the following address has permanent errors: {failedTo}. I've given up, and I will not retry the message again. """.format( failedTo=failedTo ) failedAddress = email.utils.parseaddr(failedTo)[1] data = { "boundary": "{}_{}_{}".format(time.time(), os.getpid(), "XXXXX"), "ctime": time.ctime(time.time()), "failedAddress": failedAddress, "failedDomain": failedAddress.split("@", 1)[1], "failedFrom": failedFrom, "failedTo": failedTo, "messageID": smtp.messageid(uniq="bounce"), "message": message, "transcript": transcript, } fp = StringIO() fp.write(BOUNCE_FORMAT.format(**data)) orig = message.tell() message.seek(0, SEEK_END) sz = message.tell() message.seek(orig, SEEK_SET) if sz > 10000: while 1: line = message.readline() if isinstance(line, bytes): line = line.decode(encoding) if len(line) <= 0: break fp.write(line) else: messageContent = message.read() if isinstance(messageContent, bytes): messageContent = messageContent.decode(encoding) fp.write(messageContent) return b"", failedFrom.encode(encoding), fp.getvalue().encode(encoding)
def _fix_headers(self, msg, newmsg, sign_address): """ Move some headers from C{origmsg} to C{newmsg}, delete unwanted headers from C{origmsg} and add new headers to C{newms}. Outgoing messages are either encrypted and signed or just signed before being sent. Because of that, they are packed inside new messages and some manipulation has to be made on their headers. Allowed headers for passing through: - From - Date - To - Subject - Reply-To - References - In-Reply-To - Cc Headers to be added: - Message-ID (i.e. should not use origmsg's Message-Id) - Received (this is added automatically by twisted smtp API) - OpenPGP (see #4447) Headers to be deleted: - User-Agent :param msg: The original message. :type msg: email.message.Message :param newmsg: The new message being created. :type newmsg: email.message.Message :param sign_address: The address used to sign C{newmsg} :type sign_address: str :return: A Deferred with a touple: (new Message with the unencrypted headers, original Message with headers removed) :rtype: Deferred """ origmsg = deepcopy(msg) # move headers from origmsg to newmsg headers = origmsg.items() passthrough = [ 'from', 'date', 'to', 'subject', 'reply-to', 'references', 'in-reply-to', 'cc' ] headers = filter(lambda x: x[0].lower() in passthrough, headers) for hkey, hval in headers: newmsg.add_header(hkey, hval) del (origmsg[hkey]) # add a new message-id to newmsg newmsg.add_header('Message-Id', smtp.messageid()) # delete user-agent from origmsg del (origmsg['user-agent']) def add_openpgp_header(signkey): username, domain = sign_address.split('@') newmsg.add_header( 'OpenPGP', 'id=%s' % signkey.fingerprint, url='https://%s/key/%s' % (domain, username), preference='signencrypt') return newmsg, origmsg d = self._keymanager.get_key(sign_address, OpenPGPKey, private=True) d.addCallback(add_openpgp_header) return d
def createMessage(composer, cabinet, msgRepliedTo, fromAddress, toAddresses, subject, messageBody, cc, bcc, files, createMessageObject=None): """ Create an outgoing message, format the body into MIME parts, and populate its headers. @param createMessageObject: A one-argument callable which will be invoked with a file-like object containing MIME text and which should return a Message instance associated with objects representing that MIME data. """ MC.add_charset('utf-8', None, MC.QP, 'utf-8') encode = lambda s: MH.Header(s).encode() s = S.StringIO() wrappedMsgBody = FlowedParagraph.fromRFC2646(messageBody).asRFC2646() m = MT.MIMEText(wrappedMsgBody, 'plain', 'utf-8') m.set_param("format", "flowed") fileItems = [] if files: attachmentParts = [] for storeID in files: a = composer.store.getItemByID(long(storeID)) if isinstance(a, Part): a = cabinet.createFileItem( a.getParam('filename', default=u'', header=u'content-disposition'), unicode(a.getContentType()), a.getBody(decode=True)) fileItems.append(a) attachmentParts.append(_fileItemToEmailPart(a)) m = MMP.MIMEMultipart('mixed', None, [m] + attachmentParts) m['From'] = encode(fromAddress.address) m['To'] = encode(mimeutil.flattenEmailAddresses(toAddresses)) m['Subject'] = encode(subject) m['Date'] = EU.formatdate() m['Message-ID'] = smtp.messageid('divmod.xquotient') if cc: m['Cc'] = encode(mimeutil.flattenEmailAddresses(cc)) if msgRepliedTo is not None: #our parser does not remove continuation whitespace, so to #avoid duplicating it -- refs = [ hdr.value for hdr in msgRepliedTo.impl.getHeaders("References") ] if len(refs) == 0: irt = [ hdr.value for hdr in msgRepliedTo.impl.getHeaders("In-Reply-To") ] if len(irt) == 1: refs = irt else: refs = [] msgids = msgRepliedTo.impl.getHeaders("Message-ID") for hdr in msgids: msgid = hdr.value refs.append(msgid) #As far as I can tell, the email package doesn't handle #multiple values for headers automatically, so here's some #continuation whitespace. m['References'] = u'\n\t'.join(refs) m['In-Reply-To'] = msgid break G.Generator(s).flatten(m) s.seek(0) if createMessageObject is None: def createMessageObject(messageFile): return composer.createMessageAndQueueIt(fromAddress.address, messageFile, True) msg = createMessageObject(s) # there is probably a better way than this, but there # isn't a way to associate the same file item with multiple # messages anyway, so there isn't a need to reflect that here for fileItem in fileItems: fileItem.message = msg return msg
def generateEmail(self, inviteState, calendar, orgEmail, orgCN, attendees, fromAddress, replyToAddress, toAddress, language='en'): details = self.getEventDetails(calendar, language=language) canceled = (calendar.propertyValue("METHOD") == "CANCEL") iconPath = self.getIconPath(details, canceled, language=language) with translationTo(language): msg = MIMEMultipart() msg["From"] = fromAddress msg["Reply-To"] = replyToAddress msg["To"] = toAddress msg["Date"] = rfc822date() msgId = messageid() msg["Message-ID"] = msgId if canceled: formatString = _("Event canceled: %(summary)s") elif inviteState == "new": formatString = _("Event invitation: %(summary)s") elif inviteState == "update": formatString = _("Event update: %(summary)s") else: formatString = _("Event reply: %(summary)s") # The translations we get back from gettext are utf-8 encoded # strings, so convert to unicode formatString = formatString.decode("utf-8") details['subject'] = msg['Subject'] = formatString % { 'summary' : details['summary'] } msgAlt = MIMEMultipart("alternative") msg.attach(msgAlt) # Get localized labels if canceled: details['inviteLabel'] = _("Event Canceled") else: if inviteState == "new": details['inviteLabel'] = _("Event Invitation") if inviteState == "update": details['inviteLabel'] = _("Event Update") else: details['inviteLabel'] = _("Event Reply") details['dateLabel'] = _("Date") details['timeLabel'] = _("Time") details['durationLabel'] = _("Duration") details['recurrenceLabel'] = _("Occurs") details['descLabel'] = _("Description") details['orgLabel'] = _("Organizer") details['attLabel'] = _("Attendees") details['locLabel'] = _("Location") plainAttendeeList = [] for cn, mailto in attendees: if cn: plainAttendeeList.append(cn if not mailto else "%s <%s>" % (cn, mailto)) elif mailto: plainAttendeeList.append("<%s>" % (mailto,)) details['plainAttendees'] = ", ".join(plainAttendeeList) details['plainOrganizer'] = (orgCN if not orgEmail else "%s <%s>" % (orgCN, orgEmail)) # The translations we get back from gettext are utf-8 encoded # strings, so convert to unicode for key in details.keys(): if isinstance(details[key], str): details[key] = details[key].decode("utf-8") # plain text version if canceled: plainTemplate = u"""%(subject)s %(orgLabel)s: %(plainOrganizer)s %(dateLabel)s: %(dateInfo)s %(recurrenceInfo)s %(timeLabel)s: %(timeInfo)s %(durationInfo)s """ else: plainTemplate = u"""%(subject)s %(orgLabel)s: %(plainOrganizer)s %(locLabel)s: %(location)s %(dateLabel)s: %(dateInfo)s %(recurrenceInfo)s %(timeLabel)s: %(timeInfo)s %(durationInfo)s %(descLabel)s: %(description)s %(attLabel)s: %(plainAttendees)s """ plainText = plainTemplate % details msgPlain = MIMEText(plainText.encode("UTF-8"), "plain", "UTF-8") msgAlt.attach(msgPlain) # html version msgHtmlRelated = MIMEMultipart("related", type="text/html") msgAlt.attach(msgHtmlRelated) htmlAttendees = [] for cn, mailto in attendees: if mailto: htmlAttendees.append('<a href="mailto:%s">%s</a>' % (mailto, cn)) else: htmlAttendees.append(cn) details['htmlAttendees'] = ", ".join(htmlAttendees) if orgEmail: details['htmlOrganizer'] = '<a href="mailto:%s">%s</a>' % ( orgEmail, orgCN) else: details['htmlOrganizer'] = orgCN details['iconName'] = iconName = "calicon.png" templateDir = config.Scheduling.iMIP.MailTemplatesDirectory.rstrip("/") templateName = "cancel.html" if canceled else "invite.html" templatePath = os.path.join(templateDir, templateName) if not os.path.exists(templatePath): # Fall back to built-in simple templates: if canceled: htmlTemplate = u"""<html> <body><div> <h1>%(subject)s</h1> <p> <h3>%(orgLabel)s:</h3> %(htmlOrganizer)s </p> <p> <h3>%(dateLabel)s:</h3> %(dateInfo)s %(recurrenceInfo)s </p> <p> <h3>%(timeLabel)s:</h3> %(timeInfo)s %(durationInfo)s </p> """ else: htmlTemplate = u"""<html> <body><div> <p>%(inviteLabel)s</p> <h1>%(summary)s</h1> <p> <h3>%(orgLabel)s:</h3> %(htmlOrganizer)s </p> <p> <h3>%(locLabel)s:</h3> %(location)s </p> <p> <h3>%(dateLabel)s:</h3> %(dateInfo)s %(recurrenceInfo)s </p> <p> <h3>%(timeLabel)s:</h3> %(timeInfo)s %(durationInfo)s </p> <p> <h3>%(descLabel)s:</h3> %(description)s </p> <p> <h3>%(attLabel)s:</h3> %(htmlAttendees)s </p> """ else: # HTML template file exists with open(templatePath) as templateFile: htmlTemplate = templateFile.read() htmlText = htmlTemplate % details msgHtml = MIMEText(htmlText.encode("UTF-8"), "html", "UTF-8") msgHtmlRelated.attach(msgHtml) # an image for html version if (iconPath != None and os.path.exists(iconPath) and htmlTemplate.find("cid:%(iconName)s") != -1): with open(iconPath) as iconFile: msgIcon = MIMEImage(iconFile.read(), _subtype='png;x-apple-mail-type=stationery;name="%s"' % (iconName,)) msgIcon.add_header("Content-ID", "<%s>" % (iconName,)) msgIcon.add_header("Content-Disposition", "inline;filename=%s" % (iconName,)) msgHtmlRelated.attach(msgIcon) # the icalendar attachment self.log_debug("Mail gateway sending calendar body: %s" % (str(calendar))) msgIcal = MIMEText(str(calendar), "calendar", "UTF-8") method = calendar.propertyValue("METHOD").lower() msgIcal.set_param("method", method) msgIcal.add_header("Content-ID", "<invitation.ics>") msgIcal.add_header("Content-Disposition", "inline;filename=invitation.ics") msg.attach(msgIcal) return msgId, msg.as_string()
def createMessage( composer, cabinet, msgRepliedTo, fromAddress, toAddresses, subject, messageBody, cc, bcc, files, createMessageObject=None, ): """ Create an outgoing message, format the body into MIME parts, and populate its headers. @param createMessageObject: A one-argument callable which will be invoked with a file-like object containing MIME text and which should return a Message instance associated with objects representing that MIME data. """ MC.add_charset("utf-8", None, MC.QP, "utf-8") encode = lambda s: MH.Header(s).encode() s = S.StringIO() wrappedMsgBody = FlowedParagraph.fromRFC2646(messageBody).asRFC2646() m = MT.MIMEText(wrappedMsgBody, "plain", "utf-8") m.set_param("format", "flowed") fileItems = [] if files: attachmentParts = [] for storeID in files: a = composer.store.getItemByID(long(storeID)) if isinstance(a, Part): a = cabinet.createFileItem( a.getParam("filename", default=u"", header=u"content-disposition"), unicode(a.getContentType()), a.getBody(decode=True), ) fileItems.append(a) attachmentParts.append(_fileItemToEmailPart(a)) m = MMP.MIMEMultipart("mixed", None, [m] + attachmentParts) m["From"] = encode(fromAddress.address) m["To"] = encode(mimeutil.flattenEmailAddresses(toAddresses)) m["Subject"] = encode(subject) m["Date"] = EU.formatdate() m["Message-ID"] = smtp.messageid("divmod.xquotient") if cc: m["Cc"] = encode(mimeutil.flattenEmailAddresses(cc)) if msgRepliedTo is not None: # our parser does not remove continuation whitespace, so to # avoid duplicating it -- refs = [hdr.value for hdr in msgRepliedTo.impl.getHeaders("References")] if len(refs) == 0: irt = [hdr.value for hdr in msgRepliedTo.impl.getHeaders("In-Reply-To")] if len(irt) == 1: refs = irt else: refs = [] msgids = msgRepliedTo.impl.getHeaders("Message-ID") for hdr in msgids: msgid = hdr.value refs.append(msgid) # As far as I can tell, the email package doesn't handle # multiple values for headers automatically, so here's some # continuation whitespace. m["References"] = u"\n\t".join(refs) m["In-Reply-To"] = msgid break G.Generator(s).flatten(m) s.seek(0) if createMessageObject is None: def createMessageObject(messageFile): return composer.createMessageAndQueueIt(fromAddress.address, messageFile, True) msg = createMessageObject(s) # there is probably a better way than this, but there # isn't a way to associate the same file item with multiple # messages anyway, so there isn't a need to reflect that here for fileItem in fileItems: fileItem.message = msg return msg
def generateMagic(self): return smtp.messageid()
def generateEmail(self, inviteState, calendar, orgEmail, orgCN, attendees, fromAddress, replyToAddress, toAddress, language='en'): """ Generate MIME text containing an iMIP invitation, cancellation, update or reply. @param inviteState: 'new', 'update', or 'reply'. @type inviteState: C{str} @param calendar: the iCalendar component to attach to the email. @type calendar: L{twistedcaldav.ical.Component} @param orgEmail: The email for the organizer, in C{localhost@domain} format, or C{None} if the organizer has no email address. @type orgEmail: C{str} or C{NoneType} @param orgCN: Common name / display name for the organizer. @type orgCN: C{unicode} @param attendees: A C{list} of 2-C{tuple}s of (common name, email address) similar to (orgEmail, orgCN). @param fromAddress: the address to use in the C{From:} header of the email. @type fromAddress: C{str} @param replyToAddress: the address to use in the C{Reply-To} header. @type replyToAddress: C{str} @param toAddress: the address to use in the C{To} header. @type toAddress: C{str} @param language: a 2-letter language code describing the target language that the email should be generated in. @type language: C{str} @return: a 2-tuple of C{str}s: (message ID, message text). The message ID is the value of the C{Message-ID} header, and the message text is the full MIME message, ready for transport over SMTP. """ details = self.getEventDetails(calendar, language=language) canceled = (calendar.propertyValue("METHOD") == "CANCEL") subjectFormat, labels = localizedLabels(language, canceled, inviteState) details.update(labels) details['subject'] = subjectFormat % {'summary' : details['summary']} plainText = self.renderPlainText(details, (orgCN, orgEmail), attendees, canceled) htmlText = self.renderHTML(details, (orgCN, orgEmail), attendees, canceled) msg = MIMEMultipart() msg["From"] = fromAddress msg["Subject"] = details['subject'] msg["Reply-To"] = replyToAddress msg["To"] = toAddress msg["Date"] = rfc822date() msgId = messageid() msg["Message-ID"] = msgId msgAlt = MIMEMultipart("alternative") msg.attach(msgAlt) # plain version msgPlain = MIMEText(plainText, "plain", "UTF-8") msgAlt.attach(msgPlain) # html version msgHtmlRelated = MIMEMultipart("related", type="text/html") msgAlt.attach(msgHtmlRelated) msgHtml = MIMEText(htmlText, "html", "UTF-8") msgHtmlRelated.attach(msgHtml) calendarText = str(calendar) # the icalendar attachment self.log.debug("Mail gateway sending calendar body: %s" % (calendarText,)) msgIcal = MIMEText(calendarText, "calendar", "UTF-8") method = calendar.propertyValue("METHOD").lower() msgIcal.set_param("method", method) msgIcal.add_header("Content-ID", "<invitation.ics>") msgIcal.add_header("Content-Disposition", "inline;filename=invitation.ics") msg.attach(msgIcal) return msgId, msg.as_string()
def generateMagic(self): return smtp.messageid()
def testMessageID(self): d = {} for i in range(1000): m = smtp.messageid('testcase') self.failIf(m in d) d[m] = None
def generateEmail(self, inviteState, calendar, orgEmail, orgCN, attendees, fromAddress, replyToAddress, toAddress, language='en'): """ Generate MIME text containing an iMIP invitation, cancellation, update or reply. @param inviteState: 'new', 'update', or 'reply'. @type inviteState: C{str} @param calendar: the iCalendar component to attach to the email. @type calendar: L{twistedcaldav.ical.Component} @param orgEmail: The email for the organizer, in C{localhost@domain} format, or C{None} if the organizer has no email address. @type orgEmail: C{str} or C{NoneType} @param orgCN: Common name / display name for the organizer. @type orgCN: C{unicode} @param attendees: A C{list} of 2-C{tuple}s of (common name, email address) similar to (orgEmail, orgCN). @param fromAddress: the address to use in the C{From:} header of the email. @type fromAddress: C{str} @param replyToAddress: the address to use in the C{Reply-To} header. @type replyToAddress: C{str} @param toAddress: the address to use in the C{To} header. @type toAddress: C{str} @param language: a 2-letter language code describing the target language that the email should be generated in. @type language: C{str} @return: a 2-tuple of C{str}s: (message ID, message text). The message ID is the value of the C{Message-ID} header, and the message text is the full MIME message, ready for transport over SMTP. """ details = self.getEventDetails(calendar, language=language) canceled = (calendar.propertyValue("METHOD") == "CANCEL") subjectFormat, labels = localizedLabels(language, canceled, inviteState) details.update(labels) details['subject'] = subjectFormat % {'summary': details['summary']} plainText = self.renderPlainText(details, (orgCN, orgEmail), attendees, canceled) htmlText = self.renderHTML(details, (orgCN, orgEmail), attendees, canceled) msg = MIMEMultipart() msg["From"] = fromAddress msg["Subject"] = details['subject'] msg["Reply-To"] = replyToAddress msg["To"] = toAddress msg["Date"] = rfc822date() msgId = messageid() msg["Message-ID"] = msgId msgAlt = MIMEMultipart("alternative") msg.attach(msgAlt) # plain version msgPlain = MIMEText(plainText, "plain", "UTF-8") msgAlt.attach(msgPlain) # html version msgHtmlRelated = MIMEMultipart("related", type="text/html") msgAlt.attach(msgHtmlRelated) msgHtml = MIMEText(htmlText, "html", "UTF-8") msgHtmlRelated.attach(msgHtml) # the icalendar attachment # Make sure we always have the timezones used in the calendar data as iMIP requires VTIMEZONE # always be present (i.e., timezones-by-reference is not allowed in iMIP). calendarText = calendar.getTextWithTimezones(includeTimezones=True) self.log.debug("Mail gateway sending calendar body: %s" % (calendarText, )) msgIcal = MIMEText(calendarText, "calendar", "UTF-8") method = calendar.propertyValue("METHOD").lower() msgIcal.set_param("method", method) msgIcal.add_header("Content-ID", "<invitation.ics>") msgIcal.add_header("Content-Disposition", "inline;filename=invitation.ics") msg.attach(msgIcal) return msgId, msg.as_string()