#!/usr/bin/env 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 from optparse import OptionParser, make_option import os import sys try: import paths except ImportError: # Prepend /usr/lib/python2.x/site-packages/TMDA/pythonlib sitedir = os.path.join(sys.prefix, 'lib', 'python'+sys.version[:3], 'site-packages', 'TMDA', 'pythonlib') sys.path.insert(0, sitedir) from TMDA import Version # option parsing opt_usage = "%prog [OPTIONS] [ RECIP ... ]" opt_desc = \ """Tag and send outgoing messages from a MUA. If one or more recipients are provided, send the message to all RECIP arguments. If none are provided, send the message to all header recipient addresses.""" opt_list = [ make_option("-c", "--config-file", metavar="FILE", dest="config_file", help= \ """Specify a different configuration file other than ~/.tmda/config"""), make_option("-O", "--filter-outgoing-file", metavar="FILE", dest="filter_outgoing", help= \ """Full pathname to your outgoing filter file. Overrides FILTER_OUTGOING in ~/.tmda/config."""), make_option("-f", metavar="SENDER", dest="sender", help="This option is silently ignored currently."), make_option("-q", "--qfilter", action="store_true", dest="qfilter", help= \ """Return 99 to qfilter so it will not run qmail-queue itself. A qmail only option."""), make_option("-M", "--filter-match", nargs=2, metavar="RECIP SENDER", dest="filter_match", help= \ """Check whether the given e-mail address matches a line in your outgoing filter and then exit. The first address given should be the message recipient, and the second is the sender (you). This option will also check for parsing errors in the filter file."""), make_option("-V", action="store_true", default=False, dest="full_version", help="show full TMDA version information and exit"), ] parser = OptionParser(option_list=opt_list, description=opt_desc, usage=opt_usage, version=Version.TMDA) (opts, args) = parser.parse_args() if opts.full_version: print Version.ALL sys.exit() if opts.config_file: os.environ['TMDARC'] = opts.config_file if opts.filter_outgoing: os.environ['TMDA_FILTER_OUTGOING'] = opts.filter_outgoing from TMDA import Cookie from TMDA import Defaults from TMDA import FilterParser from TMDA import Util from email.utils import formataddr, getaddresses, parseaddr import socket import string # Just check Defaults.FILTER_OUTGOING for syntax errors and possible # matches, and then exit. if opts.filter_match: sender = opts.filter_match[-1] recip = opts.filter_match[-2] Util.filter_match(Defaults.FILTER_OUTGOING, recip, sender) sys.exit() msgout = Util.msg_from_file(sys.stdin) orig_msgout_as_string = Util.msg_as_string(msgout) orig_msgout_size = len(orig_msgout_as_string) orig_msgout_body_as_raw_string = Util.body_as_raw_string(msgout) def message_format(fsa, ffn, type): # "angles" is the default MESSAGE_FROM_STYLE if type == 'address': return formataddr((None, fsa)) else: return formataddr((ffn, fsa)) def make_field(cookie_type, cookie_option, from_address, to_address): """ Create an email address based on a cookie type and one or more addresses. """ if cookie_type == 'default': cookie_type = Defaults.ACTION_OUTGOING if cookie_type == 'bare': # Use an untagged address. field = from_address # Optionally append the recipient address to a file. if cookie_option and cookie_option.lower() == 'append': if Defaults.BARE_APPEND: Util.append_to_file(to_address, Defaults.BARE_APPEND) if Defaults.DB_BARE_APPEND and Defaults.DB_CONNECTION: _username = Defaults.USERNAME.lower() _hostname = Defaults.HOSTNAME.lower() _sender = _username + '@' + _hostname params = FilterParser.create_sql_params( recipient=to_address.lower(), fromheader=from_address.lower(), username=_username, hostname=_hostname, sender=_sender) Util.db_insert(Defaults.DB_CONNECTION, Defaults.DB_BARE_APPEND, params) elif cookie_type == 'dated': # Send a message with a tagged (dated) address. if cookie_option: # check for timeout override os.environ['TMDA_TIMEOUT'] = cookie_option else: os.environ['TMDA_TIMEOUT'] = '' field = Cookie.make_dated_address(from_address) elif cookie_type == 'sender': # Send a message with a tagged (sender) address sender_cookie_address = cookie_option or to_address field = Cookie.make_sender_address (from_address, sender_cookie_address) elif cookie_type == 'domain': # Send a message with a tagged (sender) address using only the # domain portion of the address domain_cookie_address = (cookie_option or to_address).split('@')[-1] field = Cookie.make_sender_address (from_address, domain_cookie_address) elif cookie_type in ('as','exp','explicit') and cookie_option: # Send a message with an explicitly defined address. field = cookie_option elif cookie_type in ('ext','extension') and cookie_option: # Send a message with a tagged (extension added) address. (username, hostname) = string.split(from_address,'@') field = username + Defaults.RECIPIENT_DELIMITER + \ cookie_option + '@' + hostname elif cookie_type in ('kw','keyword') and cookie_option: # Send a message with a tagged (keyword) address. field = Cookie.make_keyword_address(from_address, cookie_option) elif cookie_type == 'python': # this is currently broken, and needs more work field = str(eval(cookie_option)) elif cookie_type == 'shell': import commands shell_stat, shell_out = commands.getstatusoutput(cookie_option) if shell_stat == 0: field = shell_out else: raise IOError, shell_out elif not cookie_type: # cookie_type == None means field is a text string field = cookie_option else: # If cookie_type is invalid, punt and use an untagged address. field = from_address return field def inject_message(resending, to_address, from_address, full_name, msg, orig_msgout_body_as_raw_string, actions, log_msg): """Hand the message off to sendmail.""" # Default, if no From: is specified, is bare. (cookie_type, cookie_option) = actions.get('from', ('bare', None)) magic_from = make_field(cookie_type, cookie_option, from_address, to_address) envelope_sender = resent_from = magic_from # Update envelope_sender with user-specified header (cookie_type, cookie_option) = actions.get('envelope', (None, None)) if cookie_type: envelope_sender = make_field(cookie_type, cookie_option, from_address, to_address) # Update resent_from with user-specified header (cookie_type, cookie_option) = actions.get('resent-from', (None, None)) if cookie_type: resent_from = make_field(cookie_type, cookie_option, from_address, to_address) # Set From: or Resent-From: to match the envelope sender address. if resending: del msg['Resent-From'] msg['Resent-From'] = message_format(resent_from, full_name, Defaults.MESSAGE_FROM_STYLE) else: del msg['From'] msg['From'] = message_format(magic_from, full_name, Defaults.MESSAGE_FROM_STYLE) # If the Mail-Followup-To header contains an untagged address, we # need to tag that as well. if msg.has_key('mail-followup-to'): mft_list = getaddresses(msg.get_all('mail-followup-to')) new_mft_list = [] for a in mft_list: emaddy = a[1] if emaddy == from_address: new_mft_list.append(magic_from) else: new_mft_list.append(emaddy) del msg['Mail-Followup-To'] msg['Mail-Followup-To'] = ', '.join(new_mft_list) # Optionally, add an `X-TMDA-Fingerprint' header. if Defaults.FINGERPRINT: hdrlist = [] for hdr in Defaults.FINGERPRINT: if hdr == 'body': hdrval = orig_msgout_body_as_raw_string else: hdrval = msg.get(hdr) if hdrval: hdrlist.append(hdrval) if hdrlist: del msg['X-TMDA-Fingerprint'] msg['X-TMDA-Fingerprint'] = (Cookie.make_fingerprint(hdrlist)) # Optionally, add some headers. Util.add_headers(msg, Defaults.ADDED_HEADERS_CLIENT) # Optionally, remove some headers. Util.purge_headers(msg, Defaults.PURGED_HEADERS_CLIENT) # Create the custom headers. custom_headers = [ h for h in actions.keys() if h not in ('from', 'envelope', 'resent-from') ] nice_headers = {} for header in custom_headers: (cookie_type, cookie_option) = actions[header] field = make_field(cookie_type, cookie_option, from_address, to_address) if not (cookie_type in ('python','shell', None)): field = message_format(field, full_name, Defaults.MESSAGE_TAG_HEADER_STYLE) nice_header = string.capwords(header.replace('-', ' ')).replace(' ', '-') nice_headers[nice_header] = field Util.add_headers(msg, nice_headers) # Optionally, log this transmission. if Defaults.LOGFILE_OUTGOING: from TMDA import MessageLogger logger = MessageLogger.MessageLogger(Defaults.LOGFILE_OUTGOING, msg, envsender = envelope_sender, envrecip = to_address, msg_size = orig_msgout_size, action_msg = log_msg) logger.write() # Inject the message. Util.sendmail(Util.msg_as_string(msg, 78), to_address, envelope_sender) # Remove any custom headers we added for this recipient from the # message object, else all subsequent recipients will receive them # unconditionally. Util.purge_headers(msg, nice_headers.keys()) ###### # Main ###### def main(): x_tmda_over = None actions = None log_msg = None if msgout.has_key('resent-from'): # We must be resending (bouncing) the message. fullname, from_address = parseaddr(msgout.get ('resent-from')) resending = 1 else: # Use the existing From: header if possible. fullname, from_address = parseaddr(msgout.get('from')) resending = None if not fullname: fullname = Defaults.FULLNAME if not from_address or len(string.split(from_address,'@')) != 2: from_address = Defaults.USERNAME + '@' + Defaults.HOSTNAME # If recipients were provided as arguments, use them. if args: address_list = args # If running through qfilter, get recipient list from QMAILRCPTS. elif os.environ.has_key('QMAILRCPTS'): address_list = string.split(string.lower (os.environ['QMAILRCPTS']),'\n')[:-1] # Otherwise get recipients from the headers. else: address_list = [] if resending: # Use Resent-To, Resent-Cc, and Resent-Bcc addresses. resent_tos = msgout.get_all('resent-to', []) resent_ccs = msgout.get_all('resent-cc', []) resent_bccs = msgout.get_all('resent-bcc', []) header_pairs = getaddresses(resent_tos + resent_ccs + resent_bccs) else: # Use To, Cc, Bcc, and Apparently-To addresses. tos = msgout.get_all('to', []) ccs = msgout.get_all('cc', []) bccs = msgout.get_all('bcc', []) apparently_tos = msgout.get_all('apparently-to', []) header_pairs = getaddresses(tos + ccs + bccs + apparently_tos) for pair in header_pairs: address = pair[1] if address: address_list.append(address) # Check for the `X-TMDA' override header. if msgout.has_key('x-tmda'): x_tmda_over = 1 x_tmda = msgout.get('x-tmda') log_msg = '%s: %s' % ('X-TMDA', x_tmda) # X-TMDA should only have one field. if len(string.split(x_tmda)) == 1: actions = { 'from' : FilterParser.splitaction(x_tmda) } # Delete `X-TMDA' before sending. del msgout['x-tmda'] # Optionally, parse subject for `X-TMDA'. if (Defaults.X_TMDA_IN_SUBJECT and msgout.has_key('subject') and x_tmda_over is None): sub = msgout.get('subject') subsplit = sub.split(None, 2) if subsplit and subsplit[0].lower() == 'x-tmda': x_tmda_over = 1 actions = { 'from' : FilterParser.splitaction(subsplit[1]) } log_msg = '%s: %s' % ('X-TMDA', subsplit[1]) # Fixup Subject: before sending. del msgout['Subject'] if subsplit[2:]: msgout['Subject'] = subsplit[2:][0] else: msgout['Subject'] = '' # Transformations that should happen a total of once for the # message, not once per recipient of the message. # Prepend a Received header. if os.environ.has_key('TMDA_OFMIPD_RECEIVED'): # tmda-ofmipd msgout._headers.insert(0, ('Received', (os.environ.get('TMDA_OFMIPD_RECEIVED')))) else: # tmda-sendmail/inject msgout._headers.insert(0, ('Received', 'by %s (tmda-sendmail, from uid %s); %s' \ % (socket.getfqdn(), os.getuid(), Util.make_date()))) # Possibly add a `Date' field. if ((Defaults.TMDAINJECT and 'd' in list(Defaults.TMDAINJECT)) or not msgout.has_key('date')): del msgout['Date'] msgout['Date'] = Util.make_date() # Possibly add a `Message-ID' field. if ((Defaults.TMDAINJECT and 'i' in list(Defaults.TMDAINJECT)) or not msgout.has_key('message-id')): del msgout['Message-ID'] msgout['Message-ID'] = Util.make_msgid() # Add an `X-Delivery-Agent' header. del msgout['X-Delivery-Agent'] msgout['X-Delivery-Agent'] = 'TMDA/%s (%s)' % (Version.TMDA, Version.CODENAME) # Possibly add a Mail-Followup-To field if one doesn't exist. if Defaults.MAIL_FOLLOWUP_TO and not msgout.has_key('mail-followup-to'): if isinstance(Defaults.MAIL_FOLLOWUP_TO, str): if os.path.isfile(Defaults.MAIL_FOLLOWUP_TO): mft_addrs = file(Defaults.MAIL_FOLLOWUP_TO, 'r').readlines() else: mft_addrs = None else: # assume a list mft_addrs = Defaults.MAIL_FOLLOWUP_TO if mft_addrs: mft_addrs_lower = [a.lower().strip() for a in mft_addrs] toccs = getaddresses(msgout.get_all('to', []) + msgout.get_all('cc', [])) tocc_addrs = [a[1].strip() for a in toccs] tocc_addrs_lower = [a.lower() for a in tocc_addrs] for a in tocc_addrs_lower: if a in mft_addrs_lower: msgout['Mail-Followup-To'] = ', '.join(tocc_addrs) break # If the address matches a line in the filter file, it is tagged # accordingly, otherwise it is tagged with the default cookie # type. for address in address_list: os.environ['TMDA_RECIPIENT'] = address os.environ['TMDA_VRECIPIENT'] = address.replace('@', '=') # If `X-TMDA' is present we are done here. if x_tmda_over: pass else: # Without `X-TMDA', we need to parse the outgoing filter file. outfilter = FilterParser.FilterParser(Defaults.DB_CONNECTION) outfilter.read(Defaults.FILTER_OUTGOING) (actions, matching_line) = outfilter.firstmatch(address, [from_address]) log_msg = matching_line if not actions: actions = { 'from' : FilterParser.splitaction(Defaults.ACTION_OUTGOING) } log_msg = '%s (%s)' % ('action_outgoing', Defaults.ACTION_OUTGOING) # The message is sent to each recipient separately so that # everyone gets the correct tag. Make sure your MUA # generates its own Message-ID: and Date: headers so they # match on multiple recipient messages. inject_message(resending, address, from_address, fullname, msgout, orig_msgout_body_as_raw_string, actions, log_msg) if opts.qfilter: sys.exit(99) else: sys.exit(Defaults.EX_OK) # This is the end my friend. if __name__ == '__main__': main()