1# Copyright (C) 2001-2018 by the Free Software Foundation, Inc. 2# 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. 7# 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. 12# 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. 17 18"""Creation/deletion hooks for the Postfix MTA.""" 19 20import os 21import pwd 22import grp 23import time 24import errno 25from stat import * 26 27from Mailman import mm_cfg 28from Mailman import Utils 29from Mailman import LockFile 30from Mailman.i18n import C_ 31from Mailman.MailList import MailList 32from Mailman.MTA.Utils import makealiases 33from Mailman.Logging.Syslog import syslog 34 35LOCKFILE = os.path.join(mm_cfg.LOCK_DIR, 'creator') 36ALIASFILE = os.path.join(mm_cfg.DATA_DIR, 'aliases') 37VIRTFILE = os.path.join(mm_cfg.DATA_DIR, 'virtual-mailman') 38# Desired mode for aliases(.db) and virtual-mailman(.db) for both creation 39# and check_perms. 40targetmode = S_IFREG | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH 41 42try: 43 True, False 44except NameError: 45 True = 1 46 False = 0 47 48 49 50def _update_maps(): 51 # Helper function to fix owner and mode. 52 def fixom(file): 53 # It's not necessary for the non-db file to be S_IROTH, but for 54 # simplicity and compatibility with check_perms, we set it. 55 stat = os.stat(file) 56 if (stat[ST_MODE] & targetmode) <> targetmode: 57 os.chmod(file, stat[ST_MODE] | targetmode) 58 dbfile = file + '.db' 59 try: 60 stat = os.stat(dbfile) 61 except OSError, e: 62 if e.errno <> errno.ENOENT: 63 raise 64 return 65 if (stat[ST_MODE] & targetmode) <> targetmode: 66 os.chmod(dbfile, stat[ST_MODE] | targetmode) 67 user = mm_cfg.MAILMAN_USER 68 if stat[ST_UID] != pwd.getpwnam(user)[2]: 69 uid = pwd.getpwnam(user)[2] 70 gid = grp.getgrnam(mm_cfg.MAILMAN_GROUP)[2] 71 os.chown(dbfile, uid, gid) 72 msg = 'command failed: %s (status: %s, %s)' 73 acmd = mm_cfg.POSTFIX_ALIAS_CMD + ' ' + ALIASFILE 74 status = (os.system(acmd) >> 8) & 0xff 75 if status: 76 errstr = os.strerror(status) 77 syslog('error', msg, acmd, status, errstr) 78 raise RuntimeError, msg % (acmd, status, errstr) 79 # Fix owner and mode of .db if needed. 80 fixom(ALIASFILE) 81 if os.path.exists(VIRTFILE): 82 vcmd = mm_cfg.POSTFIX_MAP_CMD + ' ' + VIRTFILE 83 status = (os.system(vcmd) >> 8) & 0xff 84 if status: 85 errstr = os.strerror(status) 86 syslog('error', msg, vcmd, status, errstr) 87 raise RuntimeError, msg % (vcmd, status, errstr) 88 # Fix owner and mode of .db if needed. 89 fixom(VIRTFILE) 90 91 92 93def makelock(): 94 return LockFile.LockFile(LOCKFILE) 95 96 97def _zapfile(filename): 98 # Truncate the file w/o messing with the file permissions, but only if it 99 # already exists. 100 if os.path.exists(filename): 101 fp = open(filename, 'w') 102 fp.close() 103 104 105def clear(): 106 _zapfile(ALIASFILE) 107 _zapfile(VIRTFILE) 108 109 110 111def _addlist(mlist, fp): 112 # Set up the mailman-loop address 113 loopaddr = Utils.ParseEmail(Utils.get_site_email(extra='loop'))[0] 114 loopmbox = os.path.join(mm_cfg.DATA_DIR, 'owner-bounces.mbox') 115 # Seek to the end of the text file, but if it's empty write the standard 116 # disclaimer, and the loop catch address. 117 fp.seek(0, 2) 118 if not fp.tell(): 119 print >> fp, """\ 120# This file is generated by Mailman, and is kept in sync with the 121# binary hash file aliases.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE 122# unless you know what you're doing, and can keep the two files properly 123# in sync. If you screw it up, you're on your own. 124""" 125 print >> fp, '# The ultimate loop stopper address' 126 print >> fp, '%s: %s' % (loopaddr, loopmbox) 127 print >> fp 128 # Bootstrapping. bin/genaliases must be run before any lists are created, 129 # but if no lists exist yet then mlist is None. The whole point of the 130 # exercise is to get the minimal aliases.db file into existance. 131 if mlist is None: 132 return 133 listname = mlist.internal_name() 134 fieldsz = len(listname) + len('-unsubscribe') 135 # The text file entries get a little extra info 136 print >> fp, '# STANZA START:', listname 137 print >> fp, '# CREATED:', time.ctime(time.time()) 138 # Now add all the standard alias entries 139 for k, v in makealiases(listname): 140 # Format the text file nicely 141 print >> fp, k + ':', ((fieldsz - len(k)) * ' ') + v 142 # Finish the text file stanza 143 print >> fp, '# STANZA END:', listname 144 print >> fp 145 146 147 148def _isvirtual(mlist): 149 return (mlist and mlist.host_name.lower() in 150 [d.lower() for d in mm_cfg.POSTFIX_STYLE_VIRTUAL_DOMAINS]) 151 152def _addvirtual(mlist, fp): 153 listname = mlist.internal_name() 154 fieldsz = len(listname) + len('-unsubscribe') 155 hostname = mlist.host_name 156 # Set up the mailman-loop address 157 loopaddr = Utils.get_site_email(mlist.host_name, extra='loop') 158 loopdest = Utils.ParseEmail(loopaddr)[0] 159 # And the site list posting address. 160 siteaddr = Utils.get_site_email(mlist.host_name) 161 sitedest = Utils.ParseEmail(siteaddr)[0] 162 # And the site list -owner, -bounces and -request addresses. 163 siteowneraddr = Utils.get_site_email(mlist.host_name, extra='owner') 164 siteownerdest = Utils.ParseEmail(siteowneraddr)[0] 165 sitebouncesaddr = Utils.get_site_email(mlist.host_name, extra='bounces') 166 sitebouncesdest = Utils.ParseEmail(sitebouncesaddr)[0] 167 siterequestaddr = Utils.get_site_email(mlist.host_name, extra='request') 168 siterequestdest = Utils.ParseEmail(siterequestaddr)[0] 169 if mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN: 170 loopdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN 171 sitedest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN 172 siteownerdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN 173 sitebouncesdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN 174 siterequestdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN 175 # If the site list's host_name is a virtual domain, adding the list and 176 # owner addresses to the SITE ADDRESSES will duplicate the entries in the 177 # stanza for the list. Postfix doesn't like dups so we try to comment them 178 # here, but only for the actual site list domain. 179 if (MailList(mm_cfg.MAILMAN_SITE_LIST, lock=False).host_name.lower() == 180 hostname.lower()): 181 siteaddr = '#' + siteaddr 182 siteowneraddr = '#' + siteowneraddr 183 sitebouncesaddr = '#' + sitebouncesaddr 184 siterequestaddr = '#' + siterequestaddr 185 # Seek to the end of the text file, but if it's empty write the standard 186 # disclaimer, and the loop catch address and site address. 187 fp.seek(0, 2) 188 if not fp.tell(): 189 print >> fp, """\ 190# This file is generated by Mailman, and is kept in sync with the binary hash 191# file virtual-mailman.db. YOU SHOULD NOT MANUALLY EDIT THIS FILE unless you 192# know what you're doing, and can keep the two files properly in sync. If you 193# screw it up, you're on your own. 194# 195# Note that you should already have this virtual domain set up properly in 196# your Postfix installation. See README.POSTFIX for details. 197 198# LOOP ADDRESSES START 199%s\t%s 200# LOOP ADDRESSES END 201 202# We also add the site list address in each virtual domain as that address 203# is exposed on admin and listinfo overviews, and we add the site list-owner, 204# -bounces and -request addresses as they are exposed in the list created 205# and/or password reminder email notices. 206 207# SITE ADDRESSES START 208%s\t%s 209%s\t%s 210%s\t%s 211%s\t%s 212# SITE ADDRESSES END 213""" % (loopaddr, loopdest, siteaddr, sitedest, siteowneraddr, siteownerdest, 214 sitebouncesaddr, sitebouncesdest, siterequestaddr, siterequestdest) 215 # The text file entries get a little extra info 216 print >> fp, '# STANZA START:', listname 217 print >> fp, '# CREATED:', time.ctime(time.time()) 218 # Now add all the standard alias entries 219 for k, v in makealiases(listname): 220 fqdnaddr = '%s@%s' % (k, hostname) 221 if mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN: 222 localaddr = '%s@%s' % (k, mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN) 223 else: 224 localaddr = k 225 # Format the text file nicely 226 print >> fp, fqdnaddr, ((fieldsz - len(k)) * ' '), localaddr 227 # Finish the text file stanza 228 print >> fp, '# STANZA END:', listname 229 print >> fp 230 231 232 233# Blech. 234def _check_for_virtual_loopaddr(mlist, filename): 235 loopaddr = Utils.get_site_email(mlist.host_name, extra='loop') 236 loopdest = Utils.ParseEmail(loopaddr)[0] 237 siteaddr = Utils.get_site_email(mlist.host_name) 238 sitedest = Utils.ParseEmail(siteaddr)[0] 239 siteowneraddr = Utils.get_site_email(mlist.host_name, extra='owner') 240 siteownerdest = Utils.ParseEmail(siteowneraddr)[0] 241 sitebouncesaddr = Utils.get_site_email(mlist.host_name, extra='bounces') 242 sitebouncesdest = Utils.ParseEmail(sitebouncesaddr)[0] 243 siterequestaddr = Utils.get_site_email(mlist.host_name, extra='request') 244 siterequestdest = Utils.ParseEmail(siterequestaddr)[0] 245 if mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN: 246 loopdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN 247 sitedest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN 248 siteownerdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN 249 sitebouncesdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN 250 siterequestdest += '@' + mm_cfg.VIRTUAL_MAILMAN_LOCAL_DOMAIN 251 # If the site list's host_name is a virtual domain, adding the list and 252 # owner addresses to the SITE ADDRESSES will duplicate the entries in the 253 # stanza for the list. Postfix doesn't like dups so we try to comment them 254 # here, but only for the actual site list domain. 255 if (MailList(mm_cfg.MAILMAN_SITE_LIST, lock=False).host_name.lower() == 256 mlist.host_name.lower()): 257 siteaddr = '#' + siteaddr 258 siteowneraddr = '#' + siteowneraddr 259 sitebouncesaddr = '#' + sitebouncesaddr 260 siterequestaddr = '#' + siterequestaddr 261 infp = open(filename) 262 omask = os.umask(007) 263 try: 264 outfp = open(filename + '.tmp', 'w') 265 finally: 266 os.umask(omask) 267 try: 268 # Find the start of the loop address block 269 while True: 270 line = infp.readline() 271 if not line: 272 break 273 outfp.write(line) 274 if line.startswith('# LOOP ADDRESSES START'): 275 break 276 # Now see if our domain has already been written 277 while True: 278 line = infp.readline() 279 if not line: 280 break 281 if line.startswith('# LOOP ADDRESSES END'): 282 # It hasn't 283 print >> outfp, '%s\t%s' % (loopaddr, loopdest) 284 outfp.write(line) 285 break 286 elif line.startswith(loopaddr): 287 # We just found it 288 outfp.write(line) 289 break 290 else: 291 # This isn't our loop address, so spit it out and continue 292 outfp.write(line) 293 # Now do it all again for the site list address. It must follow the 294 # loop addresses. 295 while True: 296 line = infp.readline() 297 if not line: 298 break 299 outfp.write(line) 300 if line.startswith('# SITE ADDRESSES START'): 301 break 302 # Now see if our domain has already been written 303 while True: 304 line = infp.readline() 305 if not line: 306 break 307 if line.startswith('# SITE ADDRESSES END'): 308 # It hasn't 309 print >> outfp, '%s\t%s' % (siteaddr, sitedest) 310 print >> outfp, '%s\t%s' % (siteowneraddr, siteownerdest) 311 print >> outfp, '%s\t%s' % (sitebouncesaddr, sitebouncesdest) 312 print >> outfp, '%s\t%s' % (siterequestaddr, siterequestdest) 313 outfp.write(line) 314 break 315 elif line.startswith(siteaddr) or line.startswith('#' + siteaddr): 316 # We just found it 317 outfp.write(line) 318 break 319 else: 320 # This isn't our loop address, so spit it out and continue 321 outfp.write(line) 322 outfp.writelines(infp.readlines()) 323 finally: 324 infp.close() 325 outfp.close() 326 os.rename(filename + '.tmp', filename) 327 328 329 330def _do_create(mlist, textfile, func): 331 # Crack open the plain text file 332 try: 333 fp = open(textfile, 'r+') 334 except IOError, e: 335 if e.errno <> errno.ENOENT: raise 336 omask = os.umask(007) 337 try: 338 fp = open(textfile, 'w+') 339 finally: 340 os.umask(omask) 341 try: 342 func(mlist, fp) 343 finally: 344 fp.close() 345 # Now double check the virtual plain text file 346 if func is _addvirtual: 347 _check_for_virtual_loopaddr(mlist, textfile) 348 349 350def create(mlist, cgi=False, nolock=False, quiet=False): 351 # Acquire the global list database lock. quiet flag is ignored. 352 lock = None 353 if not nolock: 354 lock = makelock() 355 lock.lock() 356 # Do the aliases file, which need to be done in any case 357 try: 358 _do_create(mlist, ALIASFILE, _addlist) 359 if _isvirtual(mlist): 360 _do_create(mlist, VIRTFILE, _addvirtual) 361 # bin/genaliases is the only one that calls create with nolock = True. 362 # Use that to only update the maps at the end of genaliases. 363 if not nolock: 364 _update_maps() 365 finally: 366 if lock: 367 lock.unlock(unconditionally=True) 368 369 370 371def _do_remove(mlist, textfile, virtualp): 372 listname = mlist.internal_name() 373 # Now do our best to filter out the proper stanza from the text file. 374 # The text file better exist! 375 outfp = None 376 try: 377 infp = open(textfile) 378 except IOError, e: 379 if e.errno <> errno.ENOENT: raise 380 # Otherwise, there's no text file to filter so we're done. 381 return 382 try: 383 omask = os.umask(007) 384 try: 385 outfp = open(textfile + '.tmp', 'w') 386 finally: 387 os.umask(omask) 388 filteroutp = False 389 start = '# STANZA START: ' + listname 390 end = '# STANZA END: ' + listname 391 oops = '# STANZA START: ' 392 while 1: 393 line = infp.readline() 394 if not line: 395 break 396 # If we're filtering out a stanza, just look for the end marker and 397 # filter out everything in between. If we're not in the middle of 398 # filtering out a stanza, we're just looking for the proper begin 399 # marker. 400 if filteroutp: 401 if line.strip() == end: 402 filteroutp = False 403 # Discard the trailing blank line, but don't worry if 404 # we're at the end of the file. 405 infp.readline() 406 elif line.startswith(oops): 407 # Stanza end must be missing - start writing from here. 408 filteroutp = False 409 outfp.write(line) 410 # Otherwise, ignore the line 411 else: 412 if line.strip() == start: 413 # Filter out this stanza 414 filteroutp = True 415 else: 416 outfp.write(line) 417 # Close up shop, and rotate the files 418 finally: 419 infp.close() 420 outfp.close() 421 os.rename(textfile+'.tmp', textfile) 422 423 424def remove(mlist, cgi=False): 425 # Acquire the global list database lock 426 lock = makelock() 427 lock.lock() 428 try: 429 _do_remove(mlist, ALIASFILE, False) 430 if _isvirtual(mlist): 431 _do_remove(mlist, VIRTFILE, True) 432 # Regenerate the alias and map files 433 _update_maps() 434 finally: 435 lock.unlock(unconditionally=True) 436 437 438 439def checkperms(state): 440 for file in ALIASFILE, VIRTFILE: 441 if state.VERBOSE: 442 print C_('checking permissions on %(file)s') 443 stat = None 444 try: 445 stat = os.stat(file) 446 except OSError, e: 447 if e.errno <> errno.ENOENT: 448 raise 449 if stat and (stat[ST_MODE] & targetmode) <> targetmode: 450 state.ERRORS += 1 451 octmode = oct(stat[ST_MODE]) 452 print C_('%(file)s permissions must be 0664 (got %(octmode)s)'), 453 if state.FIX: 454 print C_('(fixing)') 455 os.chmod(file, stat[ST_MODE] | targetmode) 456 else: 457 print 458 # Make sure the corresponding .db files are owned by the Mailman user. 459 # We don't need to check the group ownership of the file, since 460 # check_perms checks this itself. 461 dbfile = file + '.db' 462 stat = None 463 try: 464 stat = os.stat(dbfile) 465 except OSError, e: 466 if e.errno <> errno.ENOENT: 467 raise 468 continue 469 if state.VERBOSE: 470 print C_('checking ownership of %(dbfile)s') 471 user = mm_cfg.MAILMAN_USER 472 ownerok = stat[ST_UID] == pwd.getpwnam(user)[2] 473 if not ownerok: 474 try: 475 owner = pwd.getpwuid(stat[ST_UID])[0] 476 except KeyError: 477 owner = 'uid %d' % stat[ST_UID] 478 print C_( 479 '%(dbfile)s owned by %(owner)s (must be owned by %(user)s'), 480 state.ERRORS += 1 481 if state.FIX: 482 print C_('(fixing)') 483 uid = pwd.getpwnam(user)[2] 484 gid = grp.getgrnam(mm_cfg.MAILMAN_GROUP)[2] 485 os.chown(dbfile, uid, gid) 486 else: 487 print 488 if stat and (stat[ST_MODE] & targetmode) <> targetmode: 489 state.ERRORS += 1 490 octmode = oct(stat[ST_MODE]) 491 print C_('%(dbfile)s permissions must be 0664 (got %(octmode)s)'), 492 if state.FIX: 493 print C_('(fixing)') 494 os.chmod(dbfile, stat[ST_MODE] | targetmode) 495 else: 496 print 497