General cleanups some of which is even tested <wink>. Mailman.LockFile module
[mailman.git] / Mailman / MTA / Postfix.py
blob1712bb638066e6b0de696d7456a805f928d60dca
1 # Copyright (C) 2001-2007 by the Free Software Foundation, Inc.
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
16 # USA.
18 """Creation/deletion hooks for the Postfix MTA."""
20 from __future__ import with_statement
22 import os
23 import grp
24 import pwd
25 import time
26 import errno
27 import logging
29 from stat import *
31 from Mailman import Utils
32 from Mailman.MTA.Utils import makealiases
33 from Mailman.configuration import config
34 from Mailman.i18n import _
35 from Mailman.lockfile import LockFile
37 LOCKFILE = os.path.join(config.LOCK_DIR, 'creator')
38 ALIASFILE = os.path.join(config.DATA_DIR, 'aliases')
39 VIRTFILE = os.path.join(config.DATA_DIR, 'virtual-mailman')
40 TRPTFILE = os.path.join(config.DATA_DIR, 'transport')
42 log = logging.getLogger('mailman.error')
46 def _update_maps():
47 msg = 'command failed: %s (status: %s, %s)'
48 if config.USE_LMTP:
49 tcmd = config.POSTFIX_MAP_CMD + ' ' + TRPTFILE
50 status = (os.system(tcmd) >> 8) & 0xff
51 if status:
52 errstr = os.strerror(status)
53 log.error(msg, tcmd, status, errstr)
54 raise RuntimeError(msg % (tcmd, status, errstr))
55 acmd = config.POSTFIX_ALIAS_CMD + ' ' + ALIASFILE
56 status = (os.system(acmd) >> 8) & 0xff
57 if status:
58 errstr = os.strerror(status)
59 log.error(msg, acmd, status, errstr)
60 raise RuntimeError(msg % (acmd, status, errstr))
61 if os.path.exists(VIRTFILE):
62 vcmd = config.POSTFIX_MAP_CMD + ' ' + VIRTFILE
63 status = (os.system(vcmd) >> 8) & 0xff
64 if status:
65 errstr = os.strerror(status)
66 log.error(msg, vcmd, status, errstr)
67 raise RuntimeError(msg % (vcmd, status, errstr))
71 def _zapfile(filename):
72 # Truncate the file w/o messing with the file permissions, but only if it
73 # already exists.
74 if os.path.exists(filename):
75 fp = open(filename, 'w')
76 fp.close()
79 def clear():
80 _zapfile(ALIASFILE)
81 _zapfile(VIRTFILE)
82 _zapfile(TRPTFILE)
86 def _addlist(mlist, fp):
87 # Set up the mailman-loop address
88 loopaddr = Utils.ParseEmail(Utils.get_site_noreply())[0]
89 loopmbox = os.path.join(config.DATA_DIR, 'owner-bounces.mbox')
90 # Seek to the end of the text file, but if it's empty write the standard
91 # disclaimer, and the loop catch address.
92 fp.seek(0, 2)
93 if not fp.tell():
94 print >> fp, """\
95 # This file is generated by Mailman, and is kept in sync with the
96 # binary hash file aliases.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE
97 # unless you know what you're doing, and can keep the two files properly
98 # in sync. If you screw it up, you're on your own.
99 """
100 print >> fp, '# The ultimate loop stopper address'
101 print >> fp, '%s: %s' % (loopaddr, loopmbox)
102 print >> fp
103 # Bootstrapping. bin/genaliases must be run before any lists are created,
104 # but if no lists exist yet then mlist is None. The whole point of the
105 # exercise is to get the minimal aliases.db file into existance.
106 if mlist is None:
107 return
108 listname = mlist.internal_name()
109 hostname = mlist.host_name
110 fieldsz = len(listname) + len('-unsubscribe')
111 # The text file entries get a little extra info
112 print >> fp, '# STANZA START: %s@%s' % (listname, hostname)
113 print >> fp, '# CREATED:', time.ctime(time.time())
114 # Now add all the standard alias entries
115 for k, v in makealiases(mlist):
116 l = len(k)
117 if hostname in config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
118 k += config.POSTFIX_VIRTUAL_SEPARATOR + hostname
119 # Format the text file nicely
120 print >> fp, k + ':', ((fieldsz - l) * ' ') + v
121 # Finish the text file stanza
122 print >> fp, '# STANZA END: %s@%s' % (listname, hostname)
123 print >> fp
127 def _addvirtual(mlist, fp):
128 listname = mlist.internal_name()
129 fieldsz = len(listname) + len('-unsubscribe')
130 hostname = mlist.host_name
131 # Set up the mailman-loop address
132 loopaddr = mlist.no_reply_address
133 loopdest = Utils.ParseEmail(loopaddr)[0]
134 # Seek to the end of the text file, but if it's empty write the standard
135 # disclaimer, and the loop catch address.
136 fp.seek(0, 2)
137 if not fp.tell():
138 print >> fp, """\
139 # This file is generated by Mailman, and is kept in sync with the binary hash
140 # file virtual-mailman.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you
141 # know what you're doing, and can keep the two files properly in sync. If you
142 # screw it up, you're on your own.
144 # Note that you should already have this virtual domain set up properly in
145 # your Postfix installation. See README.POSTFIX for details.
147 # LOOP ADDRESSES START
148 %s\t%s
149 # LOOP ADDRESSES END
150 """ % (loopaddr, loopdest)
151 # The text file entries get a little extra info
152 print >> fp, '# STANZA START: %s@%s' % (listname, hostname)
153 print >> fp, '# CREATED:', time.ctime(time.time())
154 # Now add all the standard alias entries
155 for k, v in makealiases(mlist):
156 fqdnaddr = '%s@%s' % (k, hostname)
157 l = len(k)
158 # Format the text file nicely
159 if hostname in config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
160 k += config.POSTFIX_VIRTUAL_SEPARATOR + hostname
161 print >> fp, fqdnaddr, ((fieldsz - l) * ' '), k
162 # Finish the text file stanza
163 print >> fp, '# STANZA END: %s@%s' % (listname, hostname)
164 print >> fp
168 # Blech.
169 def _check_for_virtual_loopaddr(mlist, filename, func):
170 loopaddr = mlist.no_reply_address
171 loopdest = Utils.ParseEmail(loopaddr)[0]
172 if func is _addtransport:
173 loopdest = 'local:' + loopdest
174 infp = open(filename)
175 outfp = open(filename + '.tmp', 'w')
176 try:
177 # Find the start of the loop address block
178 while True:
179 line = infp.readline()
180 if not line:
181 break
182 outfp.write(line)
183 if line.startswith('# LOOP ADDRESSES START'):
184 break
185 # Now see if our domain has already been written
186 while True:
187 line = infp.readline()
188 if not line:
189 break
190 if line.startswith('# LOOP ADDRESSES END'):
191 # It hasn't
192 print >> outfp, '%s\t%s' % (loopaddr, loopdest)
193 outfp.write(line)
194 break
195 elif line.startswith(loopaddr):
196 # We just found it
197 outfp.write(line)
198 break
199 else:
200 # This isn't our loop address, so spit it out and continue
201 outfp.write(line)
202 outfp.writelines(infp.readlines())
203 finally:
204 infp.close()
205 outfp.close()
206 os.rename(filename + '.tmp', filename)
210 def _addtransport(mlist, fp):
211 # Set up the mailman-loop address
212 loopaddr = mlist.no_reply_address
213 loopdest = Utils.ParseEmail(loopaddr)[0]
214 # create/add postfix transport file for mailman
215 fp.seek(0, 2)
216 if not fp.tell():
217 print >> fp, """\
218 # This file is generated by Mailman, and is kept in sync with the
219 # binary hash file transport.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE
220 # unless you know what you're doing, and can keep the two files properly
221 # in sync. If you screw it up, you're on your own.
223 # LOOP ADDRESSES START
224 %s\tlocal:%s
225 # LOOP ADDRESSES END
226 """ % (loopaddr, loopdest)
227 # List LMTP_ONLY_DOMAINS
228 if config.LMTP_ONLY_DOMAINS:
229 print >> fp, '# LMTP ONLY DOMAINS START'
230 for dom in config.LMTP_ONLY_DOMAINS:
231 print >> fp, '%s\tlmtp:%s:%s' % (dom,
232 config.LMTP_HOST,
233 config.LMTP_PORT)
234 print >> fp, '# LMTP ONLY DOMAINS END\n'
235 listname = mlist.internal_name()
236 hostname = mlist.host_name
237 # No need of individual local part if the domain is LMTP only
238 if hostname in config.LMTP_ONLY_DOMAINS:
239 return
240 fieldsz = len(listname) + len(hostname) + len('-unsubscribe') + 1
241 # The text file entries get a little extra info
242 print >> fp, '# STANZA START: %s@%s' % (listname, hostname)
243 print >> fp, '# CREATED:', time.ctime(time.time())
244 # Now add transport entries
245 for k, v in makealiases(mlist):
246 l = len(k + hostname) + 1
247 print >> fp, '%s@%s' % (k, hostname), ((fieldsz - l) * ' ')\
248 + 'lmtp:%s:%s' % (config.LMTP_HOST, config.LMTP_PORT)
250 print >> fp, '# STANZA END: %s@%s' % (listname, hostname)
251 print >> fp
255 def _do_create(mlist, textfile, func):
256 # Crack open the plain text file
257 try:
258 fp = open(textfile, 'r+')
259 except IOError, e:
260 if e.errno <> errno.ENOENT: raise
261 fp = open(textfile, 'w+')
262 try:
263 func(mlist, fp)
264 finally:
265 fp.close()
266 # Now double check the virtual plain text file
267 if func in (_addvirtual, _addtransport):
268 _check_for_virtual_loopaddr(mlist, textfile, func)
271 def create(mlist, cgi=False, nolock=False, quiet=False):
272 # Acquire the global list database lock. quiet flag is ignored.
273 lock = None
274 if not nolock:
275 # XXX FIXME
276 lock = makelock()
277 lock.lock()
278 # Do the aliases file, which always needs to be done
279 try:
280 if config.USE_LMTP:
281 _do_create(mlist, TRPTFILE, _addtransport)
282 _do_create(None, ALIASFILE, _addlist)
283 else:
284 _do_create(mlist, ALIASFILE, _addlist)
285 if mlist.host_name in config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
286 _do_create(mlist, VIRTFILE, _addvirtual)
287 _update_maps()
288 finally:
289 if lock:
290 lock.unlock(unconditionally=True)
294 def _do_remove(mlist, textfile):
295 listname = mlist.internal_name()
296 hostname = mlist.host_name
297 # Now do our best to filter out the proper stanza from the text file.
298 # The text file better exist!
299 outfp = None
300 try:
301 infp = open(textfile)
302 except IOError, e:
303 if e.errno <> errno.ENOENT: raise
304 # Otherwise, there's no text file to filter so we're done.
305 return
306 try:
307 outfp = open(textfile + '.tmp', 'w')
308 filteroutp = False
309 start = '# STANZA START: %s@%s' % (listname, hostname)
310 end = '# STANZA END: %s@%s' % (listname, hostname)
311 while 1:
312 line = infp.readline()
313 if not line:
314 break
315 # If we're filtering out a stanza, just look for the end marker and
316 # filter out everything in between. If we're not in the middle of
317 # filtering out a stanza, we're just looking for the proper begin
318 # marker.
319 if filteroutp:
320 if line.strip() == end:
321 filteroutp = False
322 # Discard the trailing blank line, but don't worry if
323 # we're at the end of the file.
324 infp.readline()
325 # Otherwise, ignore the line
326 else:
327 if line.strip() == start:
328 # Filter out this stanza
329 filteroutp = True
330 else:
331 outfp.write(line)
332 # Close up shop, and rotate the files
333 finally:
334 infp.close()
335 outfp.close()
336 os.rename(textfile+'.tmp', textfile)
339 def remove(mlist, cgi=False):
340 # Acquire the global list database lock
341 with LockFile(LOCKFILE):
342 if config.USE_LMTP:
343 _do_remove(mlist, TRPTFILE)
344 else:
345 _do_remove(mlist, ALIASFILE)
346 if mlist.host_name in config.POSTFIX_STYLE_VIRTUAL_DOMAINS:
347 _do_remove(mlist, VIRTFILE)
348 # Regenerate the alias and map files
349 _update_maps()
350 config.db.commit()
354 def checkperms(state):
355 targetmode = S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP
356 for file in ALIASFILE, VIRTFILE, TRPTFILE:
357 if state.VERBOSE:
358 print _('checking permissions on %(file)s')
359 stat = None
360 try:
361 stat = os.stat(file)
362 except OSError, e:
363 if e.errno <> errno.ENOENT:
364 raise
365 if stat and (stat[ST_MODE] & targetmode) <> targetmode:
366 state.ERRORS += 1
367 octmode = oct(stat[ST_MODE])
368 print _('%(file)s permissions must be 066x (got %(octmode)s)'),
369 if state.FIX:
370 print _('(fixing)')
371 os.chmod(file, stat[ST_MODE] | targetmode)
372 else:
373 print
374 # Make sure the corresponding .db files are owned by the Mailman user.
375 # We don't need to check the group ownership of the file, since
376 # check_perms checks this itself.
377 dbfile = file + '.db'
378 stat = None
379 try:
380 stat = os.stat(dbfile)
381 except OSError, e:
382 if e.errno <> errno.ENOENT:
383 raise
384 continue
385 if state.VERBOSE:
386 print _('checking ownership of %(dbfile)s')
387 user = config.MAILMAN_USER
388 ownerok = stat[ST_UID] == pwd.getpwnam(user)[2]
389 if not ownerok:
390 try:
391 owner = pwd.getpwuid(stat[ST_UID])[0]
392 except KeyError:
393 owner = 'uid %d' % stat[ST_UID]
394 print _('%(dbfile)s owned by %(owner)s (must be owned by %(user)s'),
395 state.ERRORS += 1
396 if state.FIX:
397 print _('(fixing)')
398 uid = pwd.getpwnam(user)[2]
399 gid = grp.getgrnam(config.MAILMAN_GROUP)[2]
400 os.chown(dbfile, uid, gid)
401 else:
402 print
403 if stat and (stat[ST_MODE] & targetmode) <> targetmode:
404 state.ERRORS += 1
405 octmode = oct(stat[ST_MODE])
406 print _('%(dbfile)s permissions must be 066x (got %(octmode)s)'),
407 if state.FIX:
408 print _('(fixing)')
409 os.chmod(dbfile, stat[ST_MODE] | targetmode)
410 else:
411 print