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