# -*- python -*- # # Copyright (C) 2001-2007 Jason R. Mastaler # # This file is part of TMDA. # # TMDA is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. A copy of this license should # be included in the file COPYING. # # TMDA is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License # for more details. # # You should have received a copy of the GNU General Public License # along with TMDA; if not, write to the Free Software Foundation, Inc., # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA """Manage a TMDA-style Auto Response.""" from email import message_from_string from email.charset import add_alias from email.errors import MessageError from email.header import Header, decode_header from email.mime.message import MIMEMessage from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.utils import formataddr, parseaddr import os import time import Defaults import Util import Version DEFAULT_CHARSET = 'US-ASCII' # Extend Charset.ALIASES with some charsets which don't already have # convenient aliases. add_alias('cyrillic', 'iso-8859-5') add_alias('greek', 'iso-8859-7') add_alias('hebrew', 'iso-8859-8') add_alias('japanese', 'euc-jp') add_alias('korean', 'euc-kr') add_alias('russian', 'koi8-r') add_alias('thai', 'tis-620') add_alias('turkish', 'iso-8859-9') add_alias('vietnamese', 'viscii') class AutoResponse: def __init__(self, msgin, bouncetext, response_type, recipient): """ msgin is an email.message object representing the incoming message we are responding to. bouncetext is a string of rfc822 headers/body created from a TMDA template. response_type is the type of auto response we should send ('request' is a confirmation request, 'accept' is a confirmation acceptance notice, and 'bounce' is a failure notice). recipient is the recipient e-mail address of this auto response. Normally the envelope sender address. """ self.msgin_as_string = Util.msg_as_string(msgin) # Only do this step if the user wants to include the entire message. if Defaults.AUTORESPONSE_INCLUDE_SENDER_COPY > 1: max_msg_size = int(Defaults.CONFIRM_MAX_MESSAGE_SIZE) # Don't include the payload if it's over a certain size. if max_msg_size and max_msg_size < len(self.msgin_as_string): msgin.set_payload('[ Message body suppressed ' '(exceeded %s bytes) ]' % max_msg_size) self.msgin_as_string = Util.msg_as_string(msgin) # Now try to re-parse the message with a full parse (not a # header-only parse) and store that as self.msgin. If the full # parse fails, there is no choice but to use the header-parsed # version, so to prevent later Generator failures, we reset # AUTORESPONSE_INCLUDE_SENDER_COPY to include only the headers. try: self.msgin = message_from_string(self.msgin_as_string) except (KeyError, MessageError, TypeError, ValueError): self.msgin = msgin Defaults.AUTORESPONSE_INCLUDE_SENDER_COPY = 1 else: self.msgin = msgin self.bouncemsg = message_from_string(bouncetext) self.responsetype = response_type self.recipient = recipient self.bodycharset = self.bouncemsg.get('bodycharset') if self.bodycharset is None: self.bodycharset = DEFAULT_CHARSET def create(self): """ Create an auto response object from whole cloth. The auto response is a MIME compliant entity with either one or two bodyparts, depending on what Defaults.AUTORESPONSE_INCLUDE_SENDER_COPY is set to. In most cases, the object will look like: multipart/mixed text/plain (response text) message/rfc822 or text/rfc822-headers (sender's message) """ # Headers that users shouldn't be setting in their templates. bad_headers = ['MIME-Version', 'Content-Type', 'BodyCharset', 'Content-Transfer-Encoding', 'Content-Disposition', 'Content-Description'] for h in bad_headers: if self.bouncemsg.has_key(h): del self.bouncemsg[h] textpart = MIMEText(self.bouncemsg.get_payload(), 'plain', self.bodycharset) bodyparts = 1 + Defaults.AUTORESPONSE_INCLUDE_SENDER_COPY if bodyparts == 1: # A single text/plain entity. self.mimemsg = textpart elif bodyparts > 1: # A multipart/mixed entity with two bodyparts. self.mimemsg = MIMEMultipart('mixed') if self.responsetype == 'request': textpart['Content-Description'] = 'Confirmation Request' elif self.responsetype == 'accept': textpart['Content-Description'] = 'Confirmation Acceptance' elif self.responsetype == 'bounce': textpart['Content-Description'] = 'Failure Notice' textpart['Content-Disposition'] = 'inline' self.mimemsg.attach(textpart) if Defaults.AUTORESPONSE_INCLUDE_SENDER_COPY == 1: # include the headers only as a text/rfc822-headers part. rfc822part = MIMEText( self.msgin_as_string[:self.msgin_as_string.index('\n\n')+1], 'rfc822-headers', self.msgin.get_charsets(DEFAULT_CHARSET)[0]) rfc822part['Content-Description'] = 'Original Message Headers' elif Defaults.AUTORESPONSE_INCLUDE_SENDER_COPY == 2: # include the entire message as a message/rfc822 part. # If the message was > CONFIRM_MAX_MESSAGE_SIZE, it has already # been truncated appropriately in the constructor. rfc822part = MIMEMessage(self.msgin) rfc822part['Content-Description'] = 'Original Message' rfc822part['Content-Disposition'] = 'inline' self.mimemsg.attach(rfc822part) # RFC 2183 section 2.10 permits the use Content-Disposition in # the main body of the message. self.mimemsg['Content-Disposition'] = 'inline' # fold the template headers into the main entity. for k, v in self.bouncemsg.items(): ksplit = k.split('.', 1) if len(ksplit) == 1: hdrcharset = DEFAULT_CHARSET else: # Header.CHARSET: Value k = ksplit[0] hdrcharset = ksplit[1] # headers like `From:' which contain e-mail addresses # might need the "Fullname" portion encoded, but the # address portion must never be encoded. if k.lower() in map(lambda s: s.lower(), Defaults.TEMPLATE_EMAIL_HEADERS): name, addr = parseaddr(v) if name and hdrcharset.lower() not in ('ascii', 'us-ascii'): h = Header(name, hdrcharset, errors='replace') name = h.encode() self.mimemsg[k] = formataddr((name, addr)) # headers like `Subject:' might contain an encoded string, # so we need to decode that first before encoding the # entire header value. elif hdrcharset.lower() not in ('ascii', 'us-ascii') and \ k.lower() in map(lambda s: s.lower(), Defaults.TEMPLATE_ENCODED_HEADERS): h = Header(charset=hdrcharset, header_name=k, errors='replace') decoded_seq = decode_header(v) for s, charset in decoded_seq: h.append(s, charset) self.mimemsg[k] = h else: self.mimemsg[k] = Header(v, hdrcharset, header_name=k, errors='replace') # Add some new headers to the main entity. timesecs = time.time() self.mimemsg['Date'] = Util.make_date(timesecs) # required by RFC 2822 self.mimemsg['Message-ID'] = Util.make_msgid(timesecs) # Ditto # References refs = [] for h in ['references', 'message-id']: if self.msgin.has_key(h): refs = refs + self.msgin.get(h).split() if refs: self.mimemsg['References'] = '\n\t'.join(refs) # In-Reply-To if self.msgin.has_key('message-id'): self.mimemsg['In-Reply-To'] = self.msgin.get('message-id') self.mimemsg['To'] = self.recipient # Some auto responders respect this header. self.mimemsg['Precedence'] = 'bulk' # Auto-Submitted per draft-moore-auto-email-response-00.txt if self.responsetype in ('request', 'accept'): self.mimemsg['Auto-Submitted'] = 'auto-replied' elif self.responsetype == 'bounce': self.mimemsg['Auto-Submitted'] = 'auto-generated (failure)' self.mimemsg['X-Delivery-Agent'] = 'TMDA/%s (%s)' % (Version.TMDA, Version.CODENAME) # Optionally, add some custom headers. Util.add_headers(self.mimemsg, Defaults.ADDED_HEADERS_SERVER) # Optionally, remove some headers. Util.purge_headers(self.mimemsg, Defaults.PURGED_HEADERS_SERVER) def send(self): """ Inject the auto response into the mail transport system. """ Util.sendmail(Util.msg_as_string(self.mimemsg, 78), self.recipient, Defaults.BOUNCE_ENV_SENDER) def record(self): """ Record this auto response. Used as part of TMDA's auto response rate limiting feature, controlled by Defaults.MAX_AUTORESPONSES_PER_DAY. """ response_filename = '%s.%s.%s' % (int(time.time()), Defaults.PID, Util.normalize_sender(self.recipient)) # Create ~/.tmda/responses if necessary. if not os.path.exists(Defaults.RESPONSE_DIR): os.makedirs(Defaults.RESPONSE_DIR, 0700) fp = open(os.path.join(Defaults.RESPONSE_DIR, response_filename), 'w') fp.close()