Use my lazr.config megamerge branch for now, even though it's still under
[mailman.git] / mailman / queue / maildir.py
blob3f7dd555627498eff24e570ebf70c519b0d8c244
1 # Copyright (C) 2002-2008 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Maildir pre-queue runner.
20 Most MTAs can be configured to deliver messages to a `Maildir'[1]. This
21 runner will read messages from a maildir's new/ directory and inject them into
22 Mailman's qfiles/in directory for processing in the normal pipeline. This
23 delivery mechanism contrasts with mail program delivery, where incoming
24 messages end up in qfiles/in via the MTA executing the scripts/post script
25 (and likewise for the other -aliases for each mailing list).
27 The advantage to Maildir delivery is that it is more efficient; there's no
28 need to fork an intervening program just to take the message from the MTA's
29 standard output, to the qfiles/in directory.
31 [1] http://cr.yp.to/proto/maildir.html
33 We're going to use the :info flag == 1, experimental status flag for our own
34 purposes. The :1 can be followed by one of these letters:
36 - P means that MaildirRunner's in the process of parsing and enqueuing the
37 message. If successful, it will delete the file.
39 - X means something failed during the parse/enqueue phase. An error message
40 will be logged to log/error and the file will be renamed <filename>:1,X.
41 MaildirRunner will never automatically return to this file, but once the
42 problem is fixed, you can manually move the file back to the new/ directory
43 and MaildirRunner will attempt to re-process it. At some point we may do
44 this automatically.
46 See the variable USE_MAILDIR in Defaults.py.in for enabling this delivery
47 mechanism.
48 """
50 # NOTE: Maildir delivery is experimental in Mailman 2.1.
52 import os
53 import errno
54 import logging
56 from email.Parser import Parser
57 from email.Utils import parseaddr
59 from mailman.Message import Message
60 from mailman.config import config
61 from mailman.queue import Runner
63 log = logging.getLogger('mailman.error')
65 # We only care about the listname and the subq as in listname@ or
66 # listname-request@
67 subqnames = ('admin','bounces','confirm','join','leave',
68 'owner','request','subscribe','unsubscribe')
70 def getlistq(address):
71 localpart, domain = address.split('@', 1)
72 # TK: FIXME I only know configs of Postfix.
73 if config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
74 p = localpart.split(config.POSTFIX_VIRTUAL_SEPARATOR,1)
75 if len(p) == 2:
76 localpart, domain = p
77 l = localpart.split('-')
78 if l[-1] in subqnames:
79 listname = '-'.join(l[:-1])
80 subq = l[-1]
81 else:
82 listname = localpart
83 subq = None
84 return listname, subq, domain
87 class MaildirRunner(Runner):
88 # This class is much different than most runners because it pulls files
89 # of a different format than what scripts/post and friends leaves. The
90 # files this runner reads are just single message files as dropped into
91 # the directory by the MTA. This runner will read the file, and enqueue
92 # it in the expected qfiles directory for normal processing.
93 def __init__(self, slice=None, numslices=1):
94 # Don't call the base class constructor, but build enough of the
95 # underlying attributes to use the base class's implementation.
96 self._stop = 0
97 self._dir = os.path.join(config.MAILDIR_DIR, 'new')
98 self._cur = os.path.join(config.MAILDIR_DIR, 'cur')
99 self._parser = Parser(Message)
101 def _one_iteration(self):
102 # Refresh this each time through the list.
103 listnames = list(config.list_manager.names)
104 # Cruise through all the files currently in the new/ directory
105 try:
106 files = os.listdir(self._dir)
107 except OSError, e:
108 if e.errno <> errno.ENOENT: raise
109 # Nothing's been delivered yet
110 return 0
111 for file in files:
112 srcname = os.path.join(self._dir, file)
113 dstname = os.path.join(self._cur, file + ':1,P')
114 xdstname = os.path.join(self._cur, file + ':1,X')
115 try:
116 os.rename(srcname, dstname)
117 except OSError, e:
118 if e.errno == errno.ENOENT:
119 # Some other MaildirRunner beat us to it
120 continue
121 log.error('Could not rename maildir file: %s', srcname)
122 raise
123 # Now open, read, parse, and enqueue this message
124 try:
125 fp = open(dstname)
126 try:
127 msg = self._parser.parse(fp)
128 finally:
129 fp.close()
130 # Now we need to figure out which queue of which list this
131 # message was destined for. See verp_bounce() in
132 # BounceRunner.py for why we do things this way.
133 vals = []
134 for header in ('delivered-to', 'envelope-to', 'apparently-to'):
135 vals.extend(msg.get_all(header, []))
136 for field in vals:
137 to = parseaddr(field)[1].lower()
138 if not to:
139 continue
140 listname, subq, domain = getlistq(to)
141 listname = listname + '@' + domain
142 if listname in listnames:
143 break
144 else:
145 # As far as we can tell, this message isn't destined for
146 # any list on the system. What to do?
147 log.error('Message apparently not for any list: %s',
148 xdstname)
149 os.rename(dstname, xdstname)
150 continue
151 # BAW: blech, hardcoded
152 msgdata = {'listname': listname}
153 # -admin is deprecated
154 if subq in ('bounces', 'admin'):
155 queue = Switchboard(config.BOUNCEQUEUE_DIR)
156 elif subq == 'confirm':
157 msgdata['toconfirm'] = 1
158 queue = Switchboard(config.CMDQUEUE_DIR)
159 elif subq in ('join', 'subscribe'):
160 msgdata['tojoin'] = 1
161 queue = Switchboard(config.CMDQUEUE_DIR)
162 elif subq in ('leave', 'unsubscribe'):
163 msgdata['toleave'] = 1
164 queue = Switchboard(config.CMDQUEUE_DIR)
165 elif subq == 'owner':
166 msgdata.update({
167 'toowner': True,
168 'envsender': config.SITE_OWNER_ADDRESS,
169 'pipeline': config.OWNER_PIPELINE,
171 queue = Switchboard(config.INQUEUE_DIR)
172 elif subq is None:
173 msgdata['tolist'] = 1
174 queue = Switchboard(config.INQUEUE_DIR)
175 elif subq == 'request':
176 msgdata['torequest'] = 1
177 queue = Switchboard(config.CMDQUEUE_DIR)
178 else:
179 log.error('Unknown sub-queue: %s', subq)
180 os.rename(dstname, xdstname)
181 continue
182 queue.enqueue(msg, msgdata)
183 os.unlink(dstname)
184 except Exception, e:
185 os.rename(dstname, xdstname)
186 log.error('%s', e)
188 def _clean_up(self):
189 pass