# Copyright (c) 2000 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# 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.

__revision__ = "$Id: Email.py,v 1.26 2001/12/12 17:33:33 syt Exp $"

"""
Narval Email module. Refer to dtd/Email.dtd for e-mail structure.
"""
import smtplib, mailbox, multifile, mimetools, mimetypes, mimify
import os, string
from narval.lib import *
from cStringIO import StringIO

MOD_XML = '''<?xml version="1.0" encoding="ISO-8859-1"?>
<module name="Email" xmlns:al="%s">''' % AL_NS

MAIL_NS = NO_NS

# Send an e-mail ###############################################################
def send_f(args, doc) :
    import MimeWriter
    output = {}
    # mail headers
    email = args['email']
    headers = select_one(email, 'headers')
    body = select_one(email, 'body')
    encod = get_encoding()
    # send mail
    try:
        server = args['server']
        server_addr = server.getAttributeNS(NO_NS, 'name')
    except:
        server_addr = 'mail'
    attachs = []
    for a in args['attachments']:
        attachs = select_one(args, 'url/text()')
    buf = StringIO()
    to = select_one(headers,'to/text()').data.encode(encod)
    frm = "%s <%s>" % (select_one(headers,'from/name/text()').data.encode(encod),
                       select_one(headers,'from/email_addr/text()').data.encode(encod))
    try:
        cc = select_one(headers,'cc/text()').data.encode(encod)
    except SelectException :
        cc = ""
    try:
        subject = select_one(headers,'subject/text()').data.encode(encod)
    except SelectException :
        subject = "No subject"
    envelope_dest = string.split(to,', ')
    if cc != '':
        cc_list = string.split(cc,', ')
        map(lambda x,l=envelope_dest: l.append(x), cc_list)
    buf.write('From: %s\r\nTo: %s\r\nCc: %s\r\nSubject: %s\r\n' % 
              (frm, to, cc, subject))
    # mail body
    mp = body.getAttributeNS(NO_NS, 'multipart')
    if not mp == 'yes':
        if body.firstChild:
            text = '\n'+ body.firstChild.data.encode(encod)
        else:
            text = ''
    if attachs or mp =='yes':
        # multipart message
        multipart_doc = MimeWriter.MimeWriter(buf)
        multipart_doc.startmultipartbody('Mixed')
        if text:
            subwriter = multipart_doc.nextpart()
            subwriter.addheader('Content-Transfert-Encoding', get_encoding())
            if  part == parts[0]:
                subody = subwriter.startbody('text/plain')
            subody.write(text)
        if mp == 'yes':
            parts = select_many(body,'part')
            # write down parts
            for part in parts:
                subwriter = multipart_doc.nextpart()
                subwriter.addheader('Content-Transfert-Encoding',
                                    part.getAttributeNS(NO_NS,'encoding'))
                if text or part != parts[0]:
                    file_name = str(string.split(part.getAttributeNS(NO_NS,'name'), '/'))
                    subwriter.addheader('Content-Disposition',
                                        'attachement; filename = "%s"'%(file_name))
                subody = subwriter.startbody(part.getAttributeNS(NO_NS,'content_type'))
                subody.write(part.firstChild.data)
        if attachs:
            import mimetypes, urllib
            for a in attachs:
                url = select_one(a, 'url/text()')
                type, encoding = mimetypes.guess_type(url)
                if not encoding:
                    encoding = a.getAttributeNS(NO_NS,'encoding')
                if not type:
                    type = a.getAttributeNS(NO_NS,'type')
                subwriter = multipart_doc.nextpart()
                if encoding:
                    subwriter.addheader('Content-Transfert-Encoding', encoding)
                if text or parts or a != attachs[0]:
                    file_name = a.getAttributeNS(NO_NS,'name') or os.path.basename(url)
                    subwriter.addheader('Content-Disposition',
                                        'attachement; filename = "%s"'%(file_name))
                if type:
                    subody = subwriter.startbody(type)
                # read data
                try:
                    f = urllib.urlopen(normalize_url(url)[0])
                    subody.write(f.read)
                except Exception, e:
                    output.setdefault('error', []).append(error(doc, msg=e))
        multipart_doc.lastpart()
    else:
        buf.write(text)
    # signature
    try:
        sign = select_one(email,'signature/text()').data
    except SelectException:
        sign = '\n-- \nNarval your pal'

    buf.write('\r\n\r\n%s' % sign)

    send = smtplib.SMTP(server_addr)
    #send.set_debuglevel(1)
    send.sendmail(str(frm), envelope_dest, buf.getvalue())
    send.quit()
    buf.close()
    return output


MOD_XML = MOD_XML + '''<al:action name="send" func="send_f">
  <al:description lang="en">
Take an email as input and send it. Optionaly attach required file.
  </al:description>
  <al:input use="yes" id="email">
    <al:match>email</al:match>
    <al:description lang="en">
Email to send, with valid headers/body
    </al:description>
  </al:input>
  <al:input id="server" optional="yes">
    <al:description lang="en">
SMTP server where the message will be send  (default "mail")
    </al:description>
    <al:match>smtp-server[@name]</al:match>
  </al:input>
  <al:input id="attachments" list="yes" use="yes" optional="yes">
    <al:description lang="en">
Tell to attach the file located at url\text() to the mail. The email-attachment
element should optionnaly have attributes name, type, encoding which if present
will be used to create the MIME part.
    </al:description>
    <al:match>email-attachment[url/text()]</al:match>
  </al:input>
</al:action>'''


# read new mail ################################################################
def read_new_f(args, doc):
    import time, stat
    outputs = {}
    mailbox_def = args['mbox']
    force = args['force']
    if args['no-save'] :
        attach_dir = None
    elif args['path']:
        attach_dir = expand_vars(args['path'])
    else:
        attach_dir = expand_vars('$NARVAL_HOME/data/email_attachments/')

    path = mailbox_def.getAttributeNS(NO_NS, 'path')
    path = expand_vars(path)
    last_check = mailbox_def.getAttributeNS(NO_NS, 'mdate')
    if not force and last_check != '':
        last_modif = os.stat(mailbox_def.getAttributeNS(NO_NS, 'path'))[8]
        last_check = int(last_check)
        if last_check >= last_modif:
            mailbox_def.setAttributeNS(NO_NO,'mdate',last_check)
            mailbox_def.setAttributeNS(NO_NS,'persist','yes')
            outputs['mbox'] = mailbox_def
            return outputs

    file = open(path)
    mbox = UnixMailbox(file)
    list_to_write = []

    # ajoute tous les messages à liste_to_write
    msg = mbox.next()
    while msg :
        # skip internal folder data
        discard_subject = "DON'T DELETE THIS MESSAGE -- FOLDER INTERNAL DATA"
        if msg and msg.get('Subject') == discard_subject :
            msg = mbox.next()
            continue
        
        # ajoute délimiteur
        list_to_write.append('From narval %s\n' \
                             % time.asctime(time.localtime(time.time())))
        list_to_write = list_to_write + msg.headers

        # message déjà visité
        if not force and msg.get('X-Narval') :           
            print 'old message'
            list_to_write.append('\n')

        # pour les messages que Narval n'a pas visité    
        else:
            list_to_write.append('X-Narval: I read that one :-)\n')
            list_to_write.append('\n')
            m = _rfc822_to_xml(msg, doc, attach_dir)
            m.setAttributeNS(NO_NS,'type','incoming')
            outputs.setdefault('email', []).append(m)
        # Ecriture du corps du message
        msg.rewindbody()
        list_to_write = list_to_write + msg.fp.readlines()
        # au suivant !
        msg = mbox.next()
    # ferme boîte aux lettres
    file.close()
    # On remplace l'ancien document contenant les mails par un nouveau où tous
    # les messages sont marqués.
    if list_to_write:
        open(path, 'w').writelines(list_to_write)
    mailbox_def.setAttributeNS(NO_NS, 'mdate', str(os.stat(path)[8]))
    mailbox_def.setAttributeNS(NO_NS, 'persist', 'yes')
    outputs['mbox'] = mailbox_def
    return outputs


MOD_XML = MOD_XML +"""<al:action name='read_new' func='read_new_f'>
  <al:description lang="en">
Read new mails and add an X-Narval mark in headers. All attachments except
text/plain are saved (we don't want a 20Mo file in Narval memory!)
  </al:description>
  <al:input id='mbox'>
    <al:match>mailbox</al:match>
    <al:description lang="en">
Mailbox to process
    </al:description>
  </al:input>
  <al:input id='force' optional='yes'>
    <al:match>force</al:match>
    <al:description lang="en">
If this element is present, all emails present in the mailbox will be readen
    </al:description>
  </al:input>
  <al:input id='no-save' optional='yes'>
    <al:match>no-save</al:match>
    <al:description lang="en">
If this element is present, the non plain text part won't be saved.
    </al:description>
  </al:input>
  <al:input id='path' optional='yes'>
    <al:description lang="en">
Directory where attachments will be saved (default is
$NARVAL_HOME/data/email_attachments/).
    </al:description>
    <al:match>save-path</al:match>
  </al:input>
  <al:output id='output' list='yes' optional='yes'>
    <al:description lang="en">
Readen emails
    </al:description>
    <al:match>email</al:match>
  </al:output>
  <al:output id='mbox' outdates='mail-box'>
    <al:description lang="en">
New mailbox element
    </al:description>
    <al:match>mailbox</al:match>
  </al:output>
</al:action>"""
    
MOD_XML = MOD_XML + "</module>"


# Private #####################################################################
def _rfc822_to_xml(msg, doc, path) :
    encod = get_encoding()
    frm = msg.get('From')
    sub = msg.get('Subject')
    print 'You have a new mail from %s' % frm
    print 'Subject: %s' % sub
    r = doc.createElementNS(MAIL_NS,'email')
    headers = doc.createElementNS(MAIL_NS,'headers')
    r.appendChild(headers)
    r.setAttributeNS(NO_NS,'message-id',msg.get('message-id'))
    frmNode = doc.createElementNS(MAIL_NS,'from')
    name = doc.createElementNS(MAIL_NS,'name')
    name.appendChild(doc.createTextNode(unicode(frm[0], encod)))
    frmNode.appendChild(name)
    email_addr = doc.createElementNS(MAIL_NS,'email_addr')
    email_addr.appendChild(doc.createTextNode(unicode(frm[1], encod)))
    frmNode.appendChild(email_addr)
    headers.appendChild(frmNode)

    if msg.get('to') :
        to = doc.createElementNS(MAIL_NS,'to')
        to.appendChild(doc.createTextNode(unicode(msg.get('to'), encod)))
        headers.appendChild(to)

    if msg.get('cc') :
        cc = doc.createElementNS(MAIL_NS,'cc')
        cc.appendChild(doc.createTextNode(unicode(msg.get('cc'), encod)))
        headers.appendChild(cc)

    if sub:
        subject = doc.createElementNS(MAIL_NS,'subject')
        subject.appendChild(doc.createTextNode(unicode(sub, encod)))
        headers.appendChild(subject)

    if msg.get('date') :
        date = doc.createElementNS(MAIL_NS,'date')
        from time import strftime
        from rfc822 import parsedate
        datestr = strftime ('%c', parsedate (msg.get ('date')))
        date.appendChild (doc.createTextNode (unicode(datestr)))
        headers.appendChild(date)
    
    if msg.get('content-type') :
        subcontenttype = doc.createElementNS(MAIL_NS,'content-type')
        subcontenttype.appendChild(doc.createTextNode(unicode(msg.get('content-type'))))
        headers.appendChild(subcontenttype)

    if msg.get('text') :
        headers = doc.createElementNS(MAIL_NS,'text')
        headers.appendChild(doc.createTextNode(unicode(msg.get('headers'), encod)))
        headers.appendChild(text)

    b = doc.createElementNS(MAIL_NS,'body')
    if msg.getmaintype() == 'multipart':
        b.setAttributeNS(NO_NS,'multipart','yes')
        _read_multipart(msg, b, doc, path)
    else:
        b.setAttributeNS(NO_NS,'multipart','no')
        b.appendChild(doc.createTextNode(unicode(msg.fp.read(), encod)))
    r.appendChild(b)
    return r


def _read_multipart(m, b, doc, path):
    # look for boundary
    m.rewindbody()
    mf = multifile.MultiFile(m.fp)
    mf.push(m.getparam('BOUNDARY'))
    mf.next()
    # read first part (supposedly text)
    msg_text = mimetools.Message(StringIO(mf.read()))
    msg_text.rewindbody()
    part = doc.createElementNS(MAIL_NS,'part')
    part.setAttributeNS(NO_NS,'type','text')
    part.setAttributeNS(NO_NS,'content_type','text/plain')
    part.setAttributeNS(NO_NS,'encoding','8bit')
    b.appendChild(part)
    # read other parts, *** for now discard subparts ***
    while mf.next() :
        part = mimetools.Message(StringIO(mf.read()))
        encoding = part.getencoding()
        part.rewindbody()
        if part.getparam('name') :
            fichier = part.getparam('name')
        else :
            fichier = m.get('message-id')
        if encoding == '7bit' or encoding == '8bit' :
            data = part.fp.read()
        else :
            data_f = StringIO()
            mimetools.decode(part.fp,data_f,encoding)
            data = data_f.getvalue()
        p = doc.createElementNS(MAIL_NS,'part')
        p.setAttributeNS(NO_NS,'type','file')
        type = part.get('content_type')
        p.setAttributeNS(NO_NS,'content_type', type)
        p.setAttributeNS(NO_NS,'encoding',encoding)
        p.setAttributeNS(NO_NS,'name',fichier)
        if path:
            save_path = os.path.join(path, fichier)
            i = 0
            while os.path.isfile(save_path):
                i += 1
                save_path = os.path.join(path, '%s%d'%(fichier, i))
            p.setAttributeNS(NO_NS,'path', save_path)
            saved_file = open(save_path,'w')    
            saved_file.write(data)
            saved_file.close()
            p.setAttributeNS(NO_NS,'path', save_path)
        if type == 'text/plain':
            p.appendChild(doc.createTextNode(unicode(data, encoding)))
        b.appendChild(p) 


class UnixMailbox (mailbox.UnixMailbox) :
    def next(self):
        """fonction qui renvoie un couple composé de la 1ere ligne du document
        représentant le mail ainsi qu'un objet de type mimetools.Message
        contenant toutes les autres informations
        """ 
        while 1:
            self.fp.seek(self.seekp)
            try:
                self._search_start()
            except EOFError:
                self.seekp = self.fp.tell()
                return None
            start = self.fp.tell()
            self._search_end()
            self.seekp = stop = self.fp.tell()
            if start <> stop:
                break
        return mimetools.Message(mailbox._Subfile(self.fp, start, stop))

    def _isrealfromline(self, line):
        """For explanations, see
        home.netscape.com/eng/mozilla/2.0/relnotes/demo/content-length.html
        """
        return 1
    

if __name__ == "__main__" :
    print MOD_XML
    from xml.dom.ext.reader import Sax2
    doc=read_domtree_from_string(MOD_XML)
    print doc.documentElement
