1# Copyright (C) 1998-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, USA. 16 17 18"""Mixin class for putting new messages in the right place for archival. 19 20Public archives are separated from private ones. An external archival 21mechanism (eg, pipermail) should be pointed to the right places, to do the 22archival. 23""" 24 25import os 26import errno 27import traceback 28import re 29from cStringIO import StringIO 30 31from Mailman import mm_cfg 32from Mailman import Mailbox 33from Mailman import Utils 34from Mailman import Site 35from Mailman.SafeDict import SafeDict 36from Mailman.Logging.Syslog import syslog 37from Mailman.i18n import _ 38 39try: 40 True, False 41except NameError: 42 True = 1 43 False = 0 44 45 46 47def makelink(old, new): 48 try: 49 os.symlink(old, new) 50 except OSError, e: 51 if e.errno <> errno.EEXIST: 52 raise 53 54def breaklink(link): 55 try: 56 os.unlink(link) 57 except OSError, e: 58 if e.errno <> errno.ENOENT: 59 raise 60 61 62 63class Archiver: 64 # 65 # Interface to Pipermail. HyperArch.py uses this method to get the 66 # archive directory for the mailing list 67 # 68 def InitVars(self): 69 # Configurable 70 self.archive = mm_cfg.DEFAULT_ARCHIVE 71 # 0=public, 1=private: 72 self.archive_private = mm_cfg.DEFAULT_ARCHIVE_PRIVATE 73 self.archive_volume_frequency = \ 74 mm_cfg.DEFAULT_ARCHIVE_VOLUME_FREQUENCY 75 # The archive file structure by default is: 76 # 77 # archives/ 78 # private/ 79 # listname.mbox/ 80 # listname.mbox 81 # listname/ 82 # lots-of-pipermail-stuff 83 # public/ 84 # listname.mbox@ -> ../private/listname.mbox 85 # listname@ -> ../private/listname 86 # 87 # IOW, the mbox and pipermail archives are always stored in the 88 # private archive for the list. This is safe because archives/private 89 # is always set to o-rx. Public archives have a symlink to get around 90 # the private directory, pointing directly to the private/listname 91 # which has o+rx permissions. Private archives do not have the 92 # symbolic links. 93 omask = os.umask(0) 94 try: 95 try: 96 os.mkdir(self.archive_dir()+'.mbox', 02775) 97 except OSError, e: 98 if e.errno <> errno.EEXIST: raise 99 # We also create an empty pipermail archive directory into 100 # which we'll drop an empty index.html file into. This is so 101 # that lists that have not yet received a posting have 102 # /something/ as their index.html, and don't just get a 404. 103 try: 104 os.mkdir(self.archive_dir(), 02775) 105 except OSError, e: 106 if e.errno <> errno.EEXIST: raise 107 # See if there's an index.html file there already and if not, 108 # write in the empty archive notice. 109 indexfile = os.path.join(self.archive_dir(), 'index.html') 110 fp = None 111 try: 112 fp = open(indexfile) 113 except IOError, e: 114 if e.errno <> errno.ENOENT: raise 115 omask = os.umask(002) 116 try: 117 fp = open(indexfile, 'w') 118 finally: 119 os.umask(omask) 120 fp.write(Utils.maketext( 121 'emptyarchive.html', 122 {'listname': self.real_name, 123 'listinfo': self.GetScriptURL('listinfo', absolute=1), 124 }, mlist=self)) 125 if fp: 126 fp.close() 127 finally: 128 os.umask(omask) 129 130 def archive_dir(self): 131 return Site.get_archpath(self.internal_name()) 132 133 def ArchiveFileName(self): 134 """The mbox name where messages are left for archive construction.""" 135 return os.path.join(self.archive_dir() + '.mbox', 136 self.internal_name() + '.mbox') 137 138 def GetBaseArchiveURL(self): 139 url = self.GetScriptURL('private', absolute=1) + '/' 140 if self.archive_private: 141 return url 142 else: 143 hostname = re.match('[^:]*://([^/]*)/.*', url).group(1)\ 144 or mm_cfg.DEFAULT_URL_HOST 145 url = mm_cfg.PUBLIC_ARCHIVE_URL % { 146 'listname': self.internal_name(), 147 'hostname': hostname 148 } 149 if not url.endswith('/'): 150 url += '/' 151 return url 152 153 def __archive_file(self, afn): 154 """Open (creating, if necessary) the named archive file.""" 155 omask = os.umask(002) 156 try: 157 return Mailbox.Mailbox(open(afn, 'a+')) 158 finally: 159 os.umask(omask) 160 161 # 162 # old ArchiveMail function, retained under a new name 163 # for optional archiving to an mbox 164 # 165 def __archive_to_mbox(self, post): 166 """Retain a text copy of the message in an mbox file.""" 167 try: 168 afn = self.ArchiveFileName() 169 mbox = self.__archive_file(afn) 170 mbox.AppendMessage(post) 171 mbox.fp.close() 172 except IOError, msg: 173 syslog('error', 'Archive file access failure:\n\t%s %s', afn, msg) 174 raise 175 176 def ExternalArchive(self, ar, txt): 177 d = SafeDict({'listname': self.internal_name(), 178 'hostname': self.host_name, 179 }) 180 cmd = ar % d 181 extarch = os.popen(cmd, 'w') 182 extarch.write(txt) 183 status = extarch.close() 184 if status: 185 syslog('error', 'external archiver non-zero exit status: %d\n', 186 (status & 0xff00) >> 8) 187 188 # 189 # archiving in real time this is called from list.post(msg) 190 # 191 def ArchiveMail(self, msg): 192 """Store postings in mbox and/or pipermail archive, depending.""" 193 # Fork so archival errors won't disrupt normal list delivery 194 if mm_cfg.ARCHIVE_TO_MBOX == -1: 195 return 196 # 197 # We don't need an extra archiver lock here because we know the list 198 # itself must be locked. 199 if mm_cfg.ARCHIVE_TO_MBOX in (1, 2): 200 self.__archive_to_mbox(msg) 201 if mm_cfg.ARCHIVE_TO_MBOX == 1: 202 # Archive to mbox only. 203 return 204 txt = str(msg) 205 # should we use the internal or external archiver? 206 private_p = self.archive_private 207 if mm_cfg.PUBLIC_EXTERNAL_ARCHIVER and not private_p: 208 self.ExternalArchive(mm_cfg.PUBLIC_EXTERNAL_ARCHIVER, txt) 209 elif mm_cfg.PRIVATE_EXTERNAL_ARCHIVER and private_p: 210 self.ExternalArchive(mm_cfg.PRIVATE_EXTERNAL_ARCHIVER, txt) 211 else: 212 # use the internal archiver 213 f = StringIO(txt) 214 import HyperArch 215 h = HyperArch.HyperArchive(self) 216 h.processUnixMailbox(f) 217 h.close() 218 f.close() 219 220 # 221 # called from MailList.MailList.Save() 222 # 223 def CheckHTMLArchiveDir(self): 224 # We need to make sure that the archive directory has the right perms 225 # for public vs private. If it doesn't exist, or some weird 226 # permissions errors prevent us from stating the directory, it's 227 # pointless to try to fix the perms, so we just return -scott 228 if mm_cfg.ARCHIVE_TO_MBOX == -1: 229 # Archiving is completely disabled, don't require the skeleton. 230 return 231 pubdir = Site.get_archpath(self.internal_name(), public=True) 232 privdir = self.archive_dir() 233 pubmbox = pubdir + '.mbox' 234 privmbox = privdir + '.mbox' 235 if self.archive_private: 236 breaklink(pubdir) 237 breaklink(pubmbox) 238 else: 239 # BAW: privdir or privmbox could be nonexistant. We'd get an 240 # OSError, ENOENT which should be caught and reported properly. 241 makelink(privdir, pubdir) 242 # Only make this link if the site has enabled public mbox files 243 if mm_cfg.PUBLIC_MBOX: 244 makelink(privmbox, pubmbox) 245