[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[minion-cvs] Document, comment, and debug SMTP module.
Update of /home/minion/cvsroot/src/minion/lib/mixminion/server
In directory moria.mit.edu:/tmp/cvs-serv18917/lib/mixminion/server
Modified Files:
Modules.py
Log Message:
Document, comment, and debug SMTP module.
Give a more reasonable error (not a stack trace!) on invalid SMTP addresses.
Fix a typo in a docstring.
Make sure that "===== type iii anonymous message ends ====" always appears
on its own line.
Index: Modules.py
===================================================================
RCS file: /home/minion/cvsroot/src/minion/lib/mixminion/server/Modules.py,v
retrieving revision 1.10
retrieving revision 1.11
diff -u -d -r1.10 -r1.11
--- Modules.py 4 Jan 2003 20:42:38 -0000 1.10
+++ Modules.py 5 Jan 2003 01:32:45 -0000 1.11
@@ -33,6 +33,7 @@
from mixminion.Common import LOG, createPrivateDir, MixError, isSMTPMailbox, \
isPrintingAscii
from mixminion.BuildMessage import CompressedDataTooLong
+from mixminion.Packet import ParseError
# Return values for processMessage
DELIVER_OK = 1
@@ -387,16 +388,34 @@
#----------------------------------------------------------------------
class EmailAddressSet:
"""A set of email addresses stored on disk, for use in blacklisting email
- addresses. DOCDOC"""
- #XXXX002 test
+ addresses. The file format is line-based. Lines starting with #
+ and empty lines are ignored. Whitespace is ignored. All other
+ lines take the format 'command value', where command is one of the
+ following...
+ address: match an email address, exactly. "Address fred@fred"
+ matches "fred@fred" and 'FRED@FRED'.
+ user: match the part of an email address before the @, exactly.
+ "User fred" matches "fred@fred" and "fred@alice", but not
+ "bob@fred" or "mr-fred@alice".
+ domain: match the part of an email address after the @, exactly.
+ "Domain fred" matches "bob@fred" but not "bob@fred.com" or
+ "bob@host.fred".
+ subdomains: match the part of an email address after the @,
+ or any parent domain thereof. "Subdomains fred.com" matches
+ "bob@fred.com" and "bob@host.fred.com", but not "bob@com".
+ pattern: match the email address if the provided regex appears
+ anywhere in it. "Pattern /./" matches everything;
+ "Pattern /(..)*/" matches all addresses with an even number
+ of characters.
+ """
## Fields
- # addresses -- A dict whose keys are lowercased email addresses
- # domains -- A dict whose keys are lowercased domains ("foo.bar.baz")
- # DOCDOC sub
+ # addresses -- A dict whose keys are lowercased email addresses ("foo@bar")
+ # domains -- A dict whose keys are lowercased domains ("foo.bar.baz").
+ # If the value for a key is 'SUB', all subdomains are also included.
# users -- A dict whose keys are lowercased users ("foo")
# patterns -- A list of regular expression objects.
def __init__(self, fname=None, string=None):
- """Read the address set from a file or a string. DOCDOC"""
+ """Read the address set from a file or a string."""
if string is None:
f = open(fname, 'r')
string = f.read()
@@ -422,32 +441,33 @@
arg = line[1].strip()
if cmd == 'address':
if not isSMTPMailbox(arg):
- LOG.warn("Address %s on %s doesn't look valid to me.",
- arg, lineno)
+ raise ConfigError("Address %s on %s doesn't look valid"%(
+ arg, lineno))
self.addresses[arg.lower()] = 1
elif cmd == 'user':
if not isSMTPMailbox(arg+"@x"):
- LOG.warn("User %s on %s doesn't look valid to me.",
- arg, lineno)
+ raise ConfigError("User %s on %s doesn't look valid"%(
+ arg, lineno))
self.users[arg.lower()] = 1
elif cmd == 'domain':
if not isSMTPMailbox("x@"+arg):
- LOG.warn("Domain %s on %s doesn't look valid to me.",
- arg, lineno)
+ raise ConfigError("Domain %s on %s doesn't look valid"%(
+ arg, lineno))
if not self.domains.has_key(arg.lower()):
self.domains[arg.lower()] = 1
elif cmd == 'subdomains':
if not isSMTPMailbox("x@"+arg):
- LOG.warn("Domain %s on %s doesn't look valid to me.",
- arg, lineno)
+ raise ConfigError("Domain %s on %s doesn't look valid"%(
+ arg, lineno))
self.domains[arg.lower()] = 'SUB'
elif cmd == 'pattern':
- if not arg[0] == '/' and arg[-1] == '/':
- raise ConfigError("Pattern %s on %s is missing /s.",
- arg, lineno)
+ if arg[0] != '/' or arg[-1] != '/':
+ raise ConfigError("Pattern %s on %s is missing /s."%(
+ arg, lineno))
arg = arg[1:-1]
# FFFF As an optimization, we may be able to coalesce some
- # FFFF of these patterns. I doubt this will become
+ # FFFF of these patterns. I doubt this will become part of
+ # FFFF the critical path any time soon, though.
self.patterns.append(re.compile(arg, re.I))
else:
raise ConfigError("Unrecognized command %s on line %s",
@@ -457,29 +477,33 @@
"""Return true iff this this address set contains the address
'address'.
- The result will only be accurate if 'address' is a
- valid restricted RFC822 address as checked by isSMTPMailbox.
+ *REQUIRES* that 'address' is a valid restricted RFC822
+ address as checked by isSMTPMailbox. If not, behavior is
+ undefined.
"""
- #DOCDOC
+ # Is the address blocked?
lcaddress = address.lower()
if self.addresses.has_key(lcaddress):
return 1
+ # What about its user or domain parts?
user, dom = lcaddress.split("@", 1)
if self.users.has_key(user) or self.domains.has_key(dom):
return 1
+ # Is it the subdomain of a blocked domain?
domparts = dom.split(".")
- domparts.reverse()
- dom = domparts[0]
- if self.domains.get(
- for d in domparts[1:]:
-
+ for idx in range(len(domparts)):
+ subdom = ".".join(domparts[idx:])
+ if self.domains.get(subdom,None) == 'SUB':
+ return 1
+ # Does it match any patterns?
for pat in self.patterns:
if pat.search(address):
return 1
+ # Then it must be okay.
return 0
#----------------------------------------------------------------------
@@ -639,13 +663,14 @@
return [ mixminion.Packet.SMTP_TYPE ]
class DirectSMTPModule(SMTPModule):
- #XXXX DOCDOC!!!!
- # server
- # blacklist
- # server
- # header
- # returnAddress
- #XXXX002 test
+ """Module that delivers SMTP messages via a local MTA."""
+ ## Fields
+ # server -- Name of the MTA server.
+ # header -- The string, minus "To:"-line, that gets prepended to all
+ # outgoing messages.
+ # returnAddress -- The address to use in the "From:" line.
+ # blacklist -- An EmailAddressSet of addresses to which we refuse
+ # to deliver messages.
def __init__(self):
SMTPModule.__init__(self)
@@ -653,8 +678,8 @@
return { "Delivery/SMTP" :
{ 'Enabled' : ('REQUIRE', _parseBoolean, "no"),
'BlacklistFile' : ('ALLOW', None, None),
- 'SMTPServer' : ('ALLOW', None, 'localhost'), #Required on e
- 'Messsage' : ('ALLOW', None, ""),
+ 'SMTPServer' : ('ALLOW', None, 'localhost'),
+ 'Message' : ('ALLOW', None, ""),
'ReturnAddress': ('ALLOW', None, None), #Required on e
'SubjectLine' : ('ALLOW', None,
'Type-III Anonymous Message'),
@@ -670,7 +695,7 @@
if not sec.get(field):
raise ConfigError("Missing field %s in [Delivery/SMTP]"%field)
fn = sec.get('BlacklistFile')
- if fn and not not os.path.exists(fn):
+ if fn and not os.path.exists(fn):
raise ConfigError("Blacklist file %s seems not to exist"%fn)
if not isSMTPMailbox(sec['ReturnAddress']):
LOG.warn("Return address (%s) doesn't look like an email address",
@@ -687,18 +712,24 @@
self.blacklist = EmailAddressSet(fname=sec['BlacklistFile'])
else:
self.blacklist = None
- message = textwrap.wrap(sec.get('Message',"")).strip()
- subject = sec['Subject']
+ message = "\n".join(textwrap.wrap(sec.get('Message',""))).strip()
+ subject = sec['SubjectLine']
self.returnAddress = sec['ReturnAddress']
self.header = "From: %s\nSubject: %s\n\n%s" %(
self.returnAddress, subject, message)
+ manager.enableModule(self)
+
def processMessage(self, message, tag, exitType, address):
assert exitType == mixminion.Packet.SMTP_TYPE
LOG.trace("Received SMTP message")
# parseSMTPInfo will raise a parse error if the mailbox is invalid.
- address = mixminion.Packet.parseSMTPInfo(address).email
+ try:
+ address = mixminion.Packet.parseSMTPInfo(address).email
+ except ParseError:
+ LOG.warn("Dropping SMTP message to invalid address %r", address)
+ return DELIVER_FAIL_NORETRY
# Now, have we blacklisted this address?
if self.blacklist and self.blacklist.contains(address):
@@ -773,7 +804,11 @@
"""Insert a message into the Mixmaster queue"""
assert exitType == mixminion.Packet.SMTP_TYPE
# parseSMTPInfo will raise a parse error if the mailbox is invalid.
- info = mixminion.Packet.parseSMTPInfo(smtpAddress)
+ try:
+ info = mixminion.Packet.parseSMTPInfo(smtpAddress)
+ except ParseError:
+ LOG.warn("Dropping SMTP message to invalid address %r",smtpAddress)
+ return DELIVER_FAIL_NORETRY
msg = _escapeMessageForEmail(message, tag)
handle = self.tmpQueue.queueMessage(msg)
@@ -838,7 +873,7 @@
or a reply]
None [if the message is in plaintext]
'err' [if the message was invalid.]
- 'long' [if the message might be a zlib bomb'.
+ 'long' [if the message might be a zlib bomb'].
Returns None on an invalid message."""
m = _escapeMessage(msg, tag, text=1)
@@ -863,10 +898,15 @@
else:
tag = ""
+ if msg and msg[-1] != '\n':
+ extra_newline = "\n"
+ else:
+ extra_newline = ""
+
return """\
%s======= TYPE III ANONYMOUS MESSAGE BEGINS ========
-%s%s======== TYPE III ANONYMOUS MESSAGE ENDS =========
-""" %(junk_msg, tag, msg)
+%s%s%s======== TYPE III ANONYMOUS MESSAGE ENDS =========
+""" %(junk_msg, tag, msg, extra_newline)
def _escapeMessage(message, tag, text=0):
"""Helper: given a decoded message (and possibly its tag), determine
@@ -883,7 +923,7 @@
or a reply]
None [if the message is in plaintext]
'err' [if the message was invalid.]
- 'long' [if the message might be a zlib bomb'.
+ 'long' [if the message might be a zlib bomb'].
text -- flag: if true, non-TXT messages must be base64-encoded.
"""
if tag == 'err':