# -*- python -*-

# Mailman-Reencrypt -- a tool for creating encrypted, more-secure email lists
# version 0.09-prealpha

# (c) 2000, Mr. Bad <mr.bad@pigdog.org>
# (c) 2007 Techneome, LLC - Elizabeth Fong <efong@techneome.com>
# All rights reserved.

# Reencrypt uses the GNU Privacy Guard (GnuPG) (http://www.gnupg.org/) to
# decrypt email encrypted with the public key of a mailing list and
# re-encrypt it for all the subscribers of the list.

# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software 
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# This software is made available under the GNU Public License v 2.

import os
import re
import email
import email.Iterators

PROGRAM = 'Mailman-Reencrypt'
VERSION = '0.09-prealpha'
INCRYPT = 'mmrincrypt'
OUTCRYPT = 'mmroutcrypt'

BEGIN_PGP_LINE = '-----BEGIN PGP MESSAGE-----\n'
END_PGP_LINE = '-----END PGP MESSAGE-----\n'

BUF_SIZE = 4096

gnupg_home = '/var/spool/list/.gnupg'
gnupg = '/usr/bin/gpg'
logfile = '/tmp/mmreencrypt.log'

def process(mlist, msg, msgdata):
    import tempfile, sys, traceback

    message = str(msg).splitlines(True)
    found_pgp_begin = False
    found_pgp_end = False

    for line in message:
        if line == BEGIN_PGP_LINE:
            found_pgp_begin = True
        if line == END_PGP_LINE and found_pgp_begin:
            found_pgp_end = True
    if not is_pgp_mime_message(msg) and not found_pgp_end:
        return

    # temporary files that hold encrypted data
    # NOTE: plaintext is not stored to disk
    incryptname = tempfile.mktemp(INCRYPT)
    outcryptname = tempfile.mktemp(OUTCRYPT)

    reader = list(email.Iterators.body_line_iterator(msg))
    write_incrypt(reader, msg, incryptname)
    reencrypt(mlist, incryptname, outcryptname)
    read_outcrypt(msg, outcryptname)

    # Cleanup our files
    os.remove(incryptname)
    os.remove(outcryptname)

def copy_fobj(oin, oout):
    """Copy one file object to another"""

    buf = oin.read(BUF_SIZE)
    
    while buf:
        oout.write(buf)
        buf = oin.read(BUF_SIZE)

def write_incrypt(fp, msg, incryptname):
    """Write the body of message in fp to file named incryptname."""

    incrypt = open(incryptname, "wb")
    if is_pgp_mime_message(msg):
        write_pgp_mime_message(msg, fp, incrypt)
    else:
        try:
            while True:
                line = fp.pop(0)
                incrypt.write(line)
        except IndexError:
            pass
    incrypt.close()
    
def write_pgp_mime_message(msg, fp, incrypt):

    del msg['Content-Disposition']
    if re.match(r".*multipart/encrypted", msg['Content-Type']):
        msg['Content-Type'] = "text/plain"
    try:
        line = fp.pop(0)
        while line != BEGIN_PGP_LINE:
            line = fp.pop(0)
        incrypt.write(line)
        line = fp.pop(0)
        while line != END_PGP_LINE:
            incrypt.write(line)
            line = fp.pop(0)
        incrypt.write(line)
    except IndexError:
        return

def is_pgp_mime_message(msg):

    if re.match(r".*multipart/encrypted", msg['Content-Type']):
        return 1
    return 0

def reencrypt(mlist, infilename, outfilename):
    """decrypt file infilename,
    reencrypt to recipients and put in outfilename"""

    import pipes, string
    
    localuser = mlist.GetListEmail()
    recipients = mlist.getRegularMemberKeys()
    recipients.extend(mlist.getDigestMemberKeys())

    # pipes: it's the Unix way, man.
    t = pipes.Template()

    rootcmd = "%s --homedir %s --no-tty --batch " % (gnupg, gnupg_home)

    decryptcmd = "%s --decrypt 2>> %s" % (rootcmd, logfile)
    t.append(decryptcmd, "--")

    # FIXME: for big lists, this is gonna get ultra huge
    # Need to move this stuff to an options file
    rec = string.join(map((lambda (x): "--recipient %s" % x), recipients), " ")
    encryptcmd = "%s %s --local-user %s --armor --encrypt --sign 2>> %s" % \
            (rootcmd, rec, localuser, logfile)
    t.append(encryptcmd, "--")
    t.copy(infilename, outfilename)

def read_outcrypt(msg, outcryptname):
    """Copy the file output by GPG to our variable."""

    import cStringIO
    ostr = cStringIO.StringIO()

    # Add our header & delimiter
    msg['X-Reencrypted'] = "%s/%s" % (PROGRAM, VERSION)

    outcrypt = open(outcryptname, "rb")
    copy_fobj(outcrypt, ostr)
    outcrypt.close()
    output = ostr.getvalue()
    ostr.close()

    msg.set_payload(output)
