diff options
| author | klm | 1998-03-08 05:06:12 +0000 |
|---|---|---|
| committer | klm | 1998-03-08 05:06:12 +0000 |
| commit | 957d8bf7c672d7821c5f832d982b976ec996af1a (patch) | |
| tree | 3ef9125608467766e8928cc981783d73d56a3fa3 /modules/maillist.py | |
| parent | ce936ecf1077dc040de7b4e8f015c253ed8dbd85 (diff) | |
| download | mailman-957d8bf7c672d7821c5f832d982b976ec996af1a.tar.gz mailman-957d8bf7c672d7821c5f832d982b976ec996af1a.tar.zst mailman-957d8bf7c672d7821c5f832d982b976ec996af1a.zip | |
New privacy (ant-spam) feature - bounce_matching_headers option takes
a string with "field: matchexp" lines, and, if set, messages with
header fields matching matchexp are held for approval.
MailList.parse_matching_header_opt separates the header and matchexp
using the first ':' (and trims leading whitespace off of the
matchexp), and MailList.HasMatchingHeader() does the check. And of
course, MailList.Post() does the check,
MailList.LogMsg(): given the log category, add an entry to the log
file for that category. Files all situated in setting of (new) config
opt, LOG_DIR. (Stored the open files on a list, potentially useful
when running as a long running daemon...)
MailList.ApprovedAddMember(): gets a check for existing membership -
possible when more than one subscription for the same address is
okayed from the same pending batch of subscription requests - before
it was creating multiple subscriptions, and things blew up when
attempts were made to revoke the subscriptions...
Bracketed all potentially file-creating 'open()'s
(MailList.CreateFiles(), .Save(), .Lock(), .LogMsg()) in os.umask
suspension/resumption, so that the files are created with the intended
permissions.
Added a repr which indicates the lock state of the file. (Helpful for
my exploratory programming.)
Reworded some option descriptions.
Diffstat (limited to 'modules/maillist.py')
| -rw-r--r-- | modules/maillist.py | 196 |
1 files changed, 161 insertions, 35 deletions
diff --git a/modules/maillist.py b/modules/maillist.py index 5a50b7593..d7278b0bb 100644 --- a/modules/maillist.py +++ b/modules/maillist.py @@ -38,6 +38,7 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, MailCommandHandler.__init__(self) self._internal_name = name self._ready = 0 + self._log_files = {} # 'class': log_file_obj if name: if name not in list_names(): raise mm_err.MMUnknownListError, 'list not found' @@ -117,6 +118,8 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, self.moderated = mm_cfg.DEFAULT_MODERATED self.require_explicit_destination = \ mm_cfg.DEFAULT_REQUIRE_EXPLICIT_DESTINATION + self.bounce_matching_headers = \ + mm_cfg.DEFAULT_BOUNCE_MATCHING_HEADERS self.real_name = '%s%s' % (string.upper(self._internal_name[0]), self._internal_name[1:]) self.description = '' @@ -180,30 +183,38 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, 'Text sent to people leaving the list.' 'If empty, no unsubscribe message will be sent.'), - ('reply_goes_to_list', mm_cfg.Radio, ('Sender', 'List'), 0, - 'Replies to a post go to the sender or the list?'), + ('reply_goes_to_list', mm_cfg.Radio, ('Poster', 'List'), 0, + 'Are replies to a post directed to poster or the list?'), ('moderated', mm_cfg.Radio, ('No', 'Yes'), 0, - 'Posts have to be approved by a moderator'), + 'Anti-spam: Must posts be approved by a moderator?'), ('require_explicit_destination', mm_cfg.Radio, ('No', 'Yes'), 0, - 'Posts must have list named in destination (to, cc) field?' - ' (anti-spam)'), + 'Anti-spam: Must posts have list named in destination (to, cc) ' + ' field?'), + + # Note that leading whitespace in the matchexp is trimmed - you can + # defeat that by, eg, containing it in gratuitous square brackets. + ('bounce_matching_headers', mm_cfg.Text, ('6', '40'), 0, + 'Anti-spam: Bounce posts with header matching specified' + ' regexp; divide header name and (case-insensitive) match regexp' + ' with colon'), ('posters', mm_cfg.EmailList, (5, 30), 1, 'Email addresses whose posts are auto-approved ' '(adding anyone to this list will make this a moderated list)'), ('bad_posters', mm_cfg.EmailList, (5, 30), 1, - 'Email addresses whose posts should always be bounced until ' - 'you approve them, no matter what other options you have set' - ' (anti-spam)'), + 'Anti-spam: Email addresses whose posts should always be ' + 'bounced until you approve them, no matter what other options ' + 'you have set'), ('closed', mm_cfg.Radio, ('Anyone', 'List members', 'No one'), 0, - 'Who can view subscription list'), + 'Anti-spam: Who can view subscription list'), ('member_posting_only', mm_cfg.Radio, ('No', 'Yes'), 0, - 'Only list members can send mail to the list without approval'), + 'Anti-spam: Only list members can send mail to the list ' + 'without approval'), ('auto_subscribe', mm_cfg.Radio, ('No', 'Yes'), 0, 'Subscribes are done automatically w/o admins approval'), @@ -213,17 +224,16 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, ('None', 'Requestor confirms via email', 'Admin approves'), 0, 'Extra confirmation for off-the-web subscribes'), - ('dont_respond_to_post_requests', mm_cfg.Radio, - ('Yes', 'No'), 0, 'Send mail to the poster when his mail ' - 'is held, waiting for approval?'), + ('dont_respond_to_post_requests', mm_cfg.Radio, ('Yes', 'No'), 0, + 'Send mail to poster when their mail is held awaiting approval?'), ('filter_prog', mm_cfg.String, 40, 0, 'Program to pass text through before processing, if any? ' '(Useful, eg, for signature auto-stripping, etc...)'), ('max_num_recipients', mm_cfg.Number, 3, 0, - 'Max number of TO and CC recipients before admin approval ' - 'is required (anti-spam). Use 0 for no limit.'), + 'Anti-spam: Max number of TO and CC recipients before admin ' + 'approval is required. Use 0 for no limit.'), ('max_message_size', mm_cfg.Number, 3, 0, 'Maximum length in Kb of a message body. ' @@ -271,21 +281,31 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, def CreateFiles(self): # Touch these files so they have the right dir perms no matter what. # A "just-in-case" thing. This shouldn't have to be here. - import mm_archive - open(os.path.join(self._full_path, - mm_archive.ARCHIVE_PENDING), "a+").close() - open(os.path.join(self._full_path, - mm_archive.ARCHIVE_RETAIN), "a+").close() - open(os.path.join(mm_cfg.LOCK_DIR, '%s.lock' % - self._internal_name), 'a+').close() - open(os.path.join(self._full_path, "next-digest"), "a+").close() - open(os.path.join(self._full_path, "next-digest-topics"), "a+").close() + ou = os.umask(002) + try: + import mm_archive + open(os.path.join(self._full_path, + mm_archive.ARCHIVE_PENDING), "a+").close() + open(os.path.join(self._full_path, + mm_archive.ARCHIVE_RETAIN), "a+").close() + open(os.path.join(mm_cfg.LOCK_DIR, '%s.lock' % + self._internal_name), 'a+').close() + open(os.path.join(self._full_path, "next-digest"), "a+").close() + open(os.path.join(self._full_path, "next-digest-topics"), + "a+").close() + finally: + os.umask(ou) def Save(self): # If more than one client is manipulating the database at once, we're - # pretty hosed. That's a good reason to make this a daemon not a program. + # pretty hosed. That's a good reason to make this a daemon not a + # program. self.IsListInitialized() - file = open(os.path.join(self._full_path, 'config.db'), 'w') + ou = os.umask(002) + try: + file = open(os.path.join(self._full_path, 'config.db'), 'w') + finally: + os.umask(ou) dict = {} for (key, value) in self.__dict__.items(): if key[0] <> '_': @@ -309,6 +329,33 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, self._ready = 1 self.CheckVersion() + def LogMsg(self, kind, msg, *args): + """Append a message to the log file for messages of specified kind.""" + # For want of a better fallback, we use sys.stderr if we can't get + # a log file. We need a better way to warn of failed log access... + if self._log_files.has_key(kind): + logf = self._log_files[kind] + else: + logfn = os.path.join(mm_cfg.LOG_DIR, kind) + ou = os.umask(002) + try: + try: + logf = self._log_files[kind] = open(logfn, 'a+') + except IOError, diag: + logf = self._log_files[kind] = sys.stderr + self._log_files['config'] = sys.stderr + self.LogMsg('config', + "Access failed to log file %s, %s, " + "using sys.stderr.", + logfn, `str(diag)`) + finally: + os.umask(ou) + stamp = time.strftime("%b %d %H:%M:%S %Y", + time.localtime(time.time())) + logf.write("%s %s\n" % (stamp, msg % args)) + if hasattr(logf, 'flush'): + logf.flush() + def CheckVersion(self): if self.data_version == mm_cfg.VERSION: return @@ -353,6 +400,8 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, self.AddRequest('add_member', digest, name, password) def ApprovedAddMember(self, name, password, digest): + if self.IsMember(name): + raise mm_err.MMAlreadyAMember if digest: self.digest_members.append(name) self.digest_members.sort() @@ -407,20 +456,77 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, return 1 return 0 + def parse_matching_header_opt(self): + """Return a list of triples [(field name, regex, and line), ...].""" + # Note that leading whitespace in the matchexp is trimmed - you can + # defeat that by, eg, containing it in gratuitous square brackets. + mho = self.bounce_matching_headers + all = [] + for line in string.split(mho, '\n'): + if not string.strip(line): + # Skip blank lines. + continue + try: + h, e = re.split(":[ ]*", line) + all.append((h, e, line)) + except ValueError: + raise mm_err.MMBadConfigError, line + return all + + + def HasMatchingHeader(self, msg): + """True if named header field (case-insensitive matches regexp. + + Case insensitive. + + Returns constraint line which matches or empty string for no + matches.""" + + try: + pairs = self.parse_matching_header_opt() + except mm_err.MMBadConfigError, line: + # Whoops - some bad data got by: + self.LogMsg("config", "%s - " + "bad bounce_matching_header line %s" + % (self.real_name, `line`)) + + for field, matchexp, line in pairs: + fragments = msg.getallmatchingheaders(field) + subjs = [] + l = len(field) + for f in fragments: + # Consolidate header lines, stripping header name & whitespace. + if (len(f) > l + and f[l] == ":" + and string.lower(field) == string.lower(f[0:l])): + # Non-continuation line - trim header name: + subjs.append(f[l+1:]) + elif not subjs: + # Whoops - non-continuation that matches? + subjs.append(f) + else: + # Continuation line. + subjs[-1] = subjs[-1] + f + for s in subjs: + if re.search(matchexp, s, re.I): + return line + return 0 + #msg should be an IncomingMessage object. def Post(self, msg, approved=0): +## print "Post in" # DEBUG self.IsListInitialized() sender = msg.GetSender() # If it's the admin, which we know by the approved variable, # we can skip a large number of checks. if not approved: +## print "Post checking..." # DEBUG if len(self.bad_posters): addrs = mm_utils.FindMatchingAddresses(sender, self.bad_posters) if len(addrs): self.AddRequest('post', mm_utils.SnarfMessage(msg), - 'Post from an untrusted email address requires ' - 'moderator approval.') + 'Post from an untrusted origin.') if len(self.posters): addrs = mm_utils.FindMatchingAddresses(sender, self.posters) if not len(addrs): @@ -429,14 +535,13 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, 'moderator approval.') elif self.moderated: self.AddRequest('post', mm_utils.SnarfMessage(msg), - 'Moderated list: Moderator approval required.', + 'Moderated list.', # Add an extra arg to avoid generating an # error mail. 1) if self.member_posting_only and not self.IsMember(sender): self.AddRequest('post', mm_utils.SnarfMessage(msg), - 'Posters to the list must send mail from an ' - 'email address on the list.') + 'Postings from member addresses only.') if self.max_num_recipients > 0: recips = [] toheader = msg.getheader('to') @@ -451,8 +556,14 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, if (self.require_explicit_destination and not self.HasExplicitDest(msg)): self.AddRequest('post', mm_utils.SnarfMessage(msg), - 'Missing explicit list destination: ' - 'Admin approval required.') + 'Missing explicit list destination.') + if self.bounce_matching_headers: + triggered = self.HasMatchingHeader(msg) + if triggered: + # Darn - can't include the matching line for the admin + # message because the info would also go to the sender. + self.AddRequest('post', mm_utils.SnarfMessage(msg), + 'Suspicious header content.') if self.max_message_size > 0: if len(msg.body)/1024. > self.max_message_size: self.AddRequest('post', mm_utils.SnarfMessage(msg), @@ -483,17 +594,20 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, if self.GetUserOption(sender, mm_cfg.AcknowlegePosts): ack_post = 1 # Deliver the mail. +## print "post about to deliver" # DEBUG recipients = self.members[:] if dont_send_to_sender: recipients.remove(sender) def DeliveryEnabled(x, s=self, v=mm_cfg.DisableDelivery): return not s.GetUserOption(x, v) recipients = filter(DeliveryEnabled, recipients) +## print "post delivering" # DEBUG self.DeliverToList(msg, recipients, self.msg_header, self.msg_footer) if ack_post: self.SendPostAck(msg, sender) self.last_post_time = time.time() self.post_id = self.post_id + 1 +## print "post done" # DEBUG self.Save() def Lock(self): @@ -502,8 +616,13 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, return except AttributeError: return - self._lock_file = posixfile.open( - os.path.join(mm_cfg.LOCK_DIR, '%s.lock'% self._internal_name), 'a+') + ou = os.umask(0) + try: + self._lock_file = posixfile.open( + os.path.join(mm_cfg.LOCK_DIR, '%s.lock' % self._internal_name), + 'a+') + finally: + os.umask(ou) self._lock_file.lock('w|', 1) def Unlock(self): @@ -511,6 +630,13 @@ class MailList(MailCommandHandler, HTMLFormatter, Deliverer, ListAdmin, self._lock_file.close() self._lock_file = None + def __repr__(self): + if self._lock_file: un = "" + else: un = "un" + return ("<%s.%s %slocked instance at %s>" + % (self.__module__, self.__class__.__name__, + un, hex(id(self))[2:])) + def list_names(): """Return the names of all lists in default list directory.""" got = [] |
