328920335e83dda2c7f388540dea709574bfe5db
[mspang/vmailman.git] / Mailman / Archiver / Archiver.py
1 # Copyright (C) 1998-2003 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
20 Public archives are separated from private ones.  An external archival
21 mechanism (eg, pipermail) should be pointed to the right places, to do the
22 archival.
23 """
24
25 import os
26 import errno
27 import traceback
28 import re
29 from cStringIO import StringIO
30
31 from Mailman import mm_cfg
32 from Mailman import Mailbox
33 from Mailman import Utils
34 from Mailman import Site
35 from Mailman.SafeDict import SafeDict
36 from Mailman.Logging.Syslog import syslog
37 from Mailman.i18n import _
38
39 try:
40     True, False
41 except NameError:
42     True = 1
43     False = 0
44
45
46 \f
47 def makelink(old, new):
48     try:
49         os.symlink(old, new)
50     except OSError, e:
51         if e.errno <> errno.EEXIST:
52             raise
53
54 def breaklink(link):
55     try:
56         os.unlink(link)
57     except OSError, e:
58         if e.errno <> errno.ENOENT:
59             raise
60
61
62 \f
63 class 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)