# -*- python -*-
#
# Copyright (C) 2001-2007 Jason R. Mastaler <jason@mastaler.com>
#
# 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()
syntax highlighted by Code2HTML, v. 0.9.1