#!/usr/bin/python -tt
#
# Copyright 2011-2014 David Steele <dsteele@gmail.com>
# This file is part of gnome-gmail
# Available under the terms of the GNU General Public License version 2 or later
#
""" gnome-gmail
This script accepts an argument of a mailto url, and calls up an appropriate
GMail web page to handle the directive. It is intended to support GMail as a
GNOME Preferred Email application """

import sys
import urlparse
import urllib
import webbrowser
import imaplib
import os
import re

from email import encoders
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import mimetypes

import gconf
import gio

import locale
import gettext

locale.setlocale( locale.LC_ALL, '' )
gettext.textdomain( "gnome-gmail" )
_ = gettext.gettext


import gtk
import gtk.glade

import gnomekeyring as gkey

import dbus

class GGError( Exception ):
    """ Gnome Gmail exception """
    def __init__(self, value ):
        self.value = value
        super( GGError, self).__init__()

    def __str__( self ):
        return repr( self.value )


class myIMAP4_SSL( imaplib.IMAP4_SSL ):
    def __init__( self, host = '', port = imaplib.IMAP4_SSL_PORT, keyfile = None, certfile = None):
        imaplib.IMAP4_SSL.__init__( self, host, port, keyfile, certfile )

        imaplib.Commands['XLIST'] = ('AUTH', 'SELECTED')
        imaplib.Commands['ID'] = ('AUTH')

    def xlist(self, directory='""', pattern='*'):
        """List mailbox names in directory matching pattern.

        (typ, [data]) = <instance>.xlist(directory='""', pattern='*')

        'data' is list of XLIST responses.
        """
        name = 'XLIST'
        typ, dat = self._simple_command(name, directory, pattern)
        return self._untagged_response(typ, dat, name)

    def id( self, *args ):

        name = 'ID'
        typ, dat = self._simple_command( name, *args )
        return self._untagged_response(typ, dat, name)





class GMailIMAP( ):
    """ Handle mailto URLs that include 'attach' fields by uploading the 
    messages using IMAP """

    def __init__( self, mail_dict  ):
        self.mail_dict = mail_dict

        self.message_text = self.form_message()

    def form_message( self ):
        """ Form an RFC822 message, with an appropriate MIME attachment """

        msg = MIMEMultipart()

        for header in ( "To", "Cc", "Bcc", "Subject" ):
            if header.lower() in self.mail_dict:
                msg[ header ] = self.mail_dict[ header.lower() ][0]

        fname = os.path.split( self.mail_dict[ "attach" ][0] )[1]
        
        if "subject" not in self.mail_dict:
            msg[ "Subject" ] = _("Sending %s") % fname

        msg.preamble = _("Mime message attached")


        # loop is superfluous - mail_dict will have only one path
        for filename in self.mail_dict[ 'attach' ]:

            if( filename.find( "file://" ) == 0 ):
                filename = filename[7:]

            filepath = urlparse.urlsplit( filename ).path

            if not os.path.isfile( filepath ):
                raise GGError( "File not found - %s" % filepath )

            ctype, encoding = mimetypes.guess_type( filepath )

            if ctype is None or encoding is not None:
                ctype = 'application/octet-stream'

            maintype, subtype = ctype.split( '/', 1 )

            attach_file = open( filepath, 'rb' )

            if maintype == 'text':
                attachment = MIMEText( attach_file.read(), _subtype = subtype )
            elif maintype == 'image':
                attachment = MIMEImage( attach_file.read(), _subtype = subtype )
            elif maintype == 'audio':
                attachment = MIMEAudio( attach_file.read(), _subtype = subtype )
            else:
                attachment = MIMEBase( maintype, subtype )
                attachment.set_payload( attach_file.read() )
                encoders.encode_base64( attachment )

            attach_file.close()

            attachment.add_header( 'Content-Disposition', 'attachment', 
                filename=fname)

            msg.attach( attachment )


        return( msg.as_string() )


    def send_mail( self, user, password ):
        """ transfer the message to GMail Drafts folder, using IMAP.
        Return a message ID that can be used to reference the mail via URL"""

        imap_obj = myIMAP4_SSL( "imap.gmail.com" )

        imap_obj.login( user, password )

        # identify the client per http://code.google.com/apis/gmail/imap/
        imap_obj.id( '('
              + '"name" "gnome-gmail"'
              + ' "version" "1.8.2"'
              + ' "os" "Linux"'
              + ' "contact" "dsteele@gmail.com"'
              + ' "support-url" "http://sourceforge.net/tracker/?group_id=277145"'
              + ')' 
              )

        try:
            # I may need this again someday
            folder_name = _("Drafts")

            # xlist returns directories e.g. 
            #    (\HasNoChildren \Drafts) "/" "[Gmail]/Drafts"
            # where the first "Drafts" is a flag, and the second is localized
            drafts_ln = [ x for x in imap_obj.xlist()[1]
                                     if re.search( ' .Drafts\)', x ) ][0]

            draft_folder = re.search( '\"([^\"]+)\"$', drafts_ln ).group(1)

        except:
            imap_obj.logout()
            raise GGError( _("Unable to determine IMAP Drafts folder location") )

        try:
            # upload the message to the Drafts folder
            append_resp = imap_obj.append( draft_folder, None, None, self.message_text )
        except:
            imap_obj.logout()
            raise GGError( _("Error encountered while uploading the message") )

        try:
            # get the Google Message Id for the message, and convert to a hex string
            # http://code.google.com/apis/gmail/imap/#x-gm-msgid
            # nods to https://gist.github.com/964461

            # the append returns something like ('OK', ['[APPENDUID 9 3113] (Success)'])
            msg_uid = re.search( r'UID [0-9]+ ([0-9]+)', append_resp[1][0]).group(1)

            imap_obj.select( draft_folder )
            typ, data = imap_obj.uid( r'fetch', msg_uid, r'(X-GM-MSGID)')

            msgid_dec = re.search( r'X-GM-MSGID ([0-9]+)', data[0] ).group(1)
            msgid_hex = hex( int( msgid_dec ) )
            msgid_hex_strp = re.search( r'0x(.+)', msgid_hex ).group(1)
        except:
            imap_obj.logout()
            raise GGError( _("Error accessing message. Check the Drafts folder") )

        imap_obj.logout()

        return( msgid_hex_strp )


class Keyring(object):
    """ Access the GMail password in the Gnome keyring.
    Used for IMAP attachment uploads """
    def __init__(self, name, server, protocol, port):
        self._name = name
        self._server = server
        self._protocol = protocol
        self._port = port

        self.server_tag = "server"
        self.protocol_tag = "protocol"
        self.user_tag = "user"
        

    def has_credentials(self):
        """ Does the keyring have the password for this user? """
        try:
            attrs = {self.server_tag: self._server, 
                self.protocol_tag: self._protocol}
            items = gkey.find_items_sync(gkey.ITEM_NETWORK_PASSWORD, attrs)
            return len(items) > 0
        except gkey.DeniedError:
            return False

    def get_credentials(self, user):
        """ Get the GMail password for this user """
        attrs = {self.server_tag: self._server, 
            self.protocol_tag: self._protocol,
            self.user_tag: user }
        items = gkey.find_items_sync(gkey.ITEM_NETWORK_PASSWORD, attrs)

        return (items[0].attributes[self.user_tag], items[0].secret)

    def set_credentials(self, (user, password)):
        """ Store the GMail password in the Gnome keyring """
        attrs = {
            self.user_tag: user,
            self.server_tag: self._server,
            self.protocol_tag: self._protocol,
            }
        gkey.item_create_sync(None,
        gkey.ITEM_NETWORK_PASSWORD, self._name, attrs, password, True)

class ConfigInfo( object ):
    """ Manage configuration information in gconf and the gnome keyring """
    def __init__( self, basedir = '/apps/gnome-gmail/' ):
        self.client = gconf.client_get_default()
        self.basedir = basedir

        self.keyring = Keyring( "GMail", "imap.google.com", "imap",
             "143" )

        self.user = ""
        self.password = ""
        self.savepassword = False
        self.appsdomain = ""
        self.hideconfirmation = False
        self.suppresspreferred = False

        self.xml = None

        #self.keyring.set_credentials( (self.user, "") )

    def read_config( self ):
        """ read in all configuration information """

        self.user = self.client.get_string( self.basedir + 'user' )
        if self.user == None:
            self.user = ""

        self.savepassword = self.client.get_bool( 
            self.basedir + 'savepassword' )

        if self.savepassword == None:
            self.savepassword = False

        self.appsdomain = self.client.get_string( self.basedir + 'appsdomain' )
        if self.appsdomain == None:
            self.appsdomain = ""

        self.hideconfirmation = self.client.get_bool( 
            self.basedir + 'hideconfirmation' )
        if self.hideconfirmation == None:
            self.hideconfirmation = False

        self.suppresspreferred = self.client.get_bool( 
            self.basedir + 'suppresspreferred' )
        if self.suppresspreferred == None:
            self.suppresspreferred = False

        try:
            if self.savepassword:
                (luser, self.password ) = \
                    self.keyring.get_credentials(self.user)

            if self.password == None:
                self.password = ""
        except:
            pass

    def write_config( self ):
        """ write out all configuration information """
        self.client.set_string( self.basedir + 'user', self.user )
        self.client.set_bool( self.basedir + 'savepassword', self.savepassword )
        self.client.set_string( self.basedir + 'appsdomain', self.appsdomain )
        self.client.set_bool( self.basedir + 'hideconfirmation', \
            self.hideconfirmation )
        self.client.set_bool( self.basedir + 'suppresspreferred', \
            self.suppresspreferred )

        if self.savepassword:
            self.keyring.set_credentials( (self.user, self.password) )

    def info_complete( self ):
        """ is there enough information to work with IMAP? """
        return( len( self.password) > 0 and len( self.user) > 0  )

    #def gconftodialog( self, foo = 0, bar=0 ):
    def gconftodialog( self ):
        """ transfer existing config information to the dialog """

        self.read_config()

        user_field = self.xml.get_widget( 'entryUserName' )
        user_field.set_text( self.user )

        password_field = self.xml.get_widget( 'entryPW' )
        password_field.set_text( self.password )

        savepassword_field = self.xml.get_widget( 'checkbuttonPW' )
        savepassword_field.set_active( self.savepassword )
        
    def dialogtogconf( self ):
        """ collect configuration info from the dialog """

        user_field = self.xml.get_widget( 'entryUserName' )
        self.user = user_field.get_text()

        password_field = self.xml.get_widget( 'entryPW' )
        self.password = password_field.get_text()

        savepassword_field = self.xml.get_widget( 'checkbuttonPW' )
        self.savepassword = savepassword_field.get_active()

        self.write_config()
        
    def call_configure( self, foo, bar = 1 ):
        """ the user has asked to edit configuration parameters """

        self.dialogtogconf()

        os.system( "/usr/bin/gconf-editor /apps/gnome-gmail/appsdomain &" )

        return( True )


    def query_config_info( self ):
        """ Query the user for IMAP configuration information """

        glade_file = sys.prefix + "/share/gnome-gmail/gnomegmail.glade"
        self.xml = gtk.glade.XML( glade_file, domain="gnome-gmail" )

        config_button = self.xml.get_widget( "buttonConfigure") 
        config_button.connect( "button-press-event", self.call_configure )

        # todo - should map window focus event to gconftodialog(), 
        # but it isn't working

        self.gconftodialog()

        window = self.xml.get_widget( "dialog1" )
        response = window.run()

        if response == 1:
            self.dialogtogconf()
        else:
            raise GGError( _("User cancelled \"Send To...\"") )

        window.destroy()

        return( self.info_complete() )

    def query_set_preferred( self ):

        if( not self.suppresspreferred ):

            handler_path_base = "/desktop/gnome/url-handlers/mailto/"
            handler_path = handler_path_base + "command"

            our_handler = "gnome-gmail %s"

            current_handler = self.client.get_string( handler_path )

            if( our_handler != current_handler ):
                glade_file = sys.prefix + "/share/gnome-gmail/gnomegmail.glade"
                self.xml = gtk.glade.XML( glade_file, domain="gnome-gmail" )

                window = self.xml.get_widget( "preferred_app_dialog" )
                response = window.run()

                if( response == 1 ):
                    self.client.set_string( handler_path, our_handler )
                    self.client.set_bool( handler_path_base + "enabled", True )
                    self.client.set_bool( handler_path_base + "needs_terminal",\
                        False )

                ask_check_button = self.xml.get_widget( "check_dont_ask_again" )
                self.suppresspreferred = ask_check_button.get_active()

                self.write_config()


class GMailURL( ):
    """ Logic to convert a mailto link to an appropriate GMail URL, by
    any means necessary, including IMAP uploads, if necessary """

    def __init__( self, mailto_url, cfg, enable_net_access = True ):
        self.mailto_url = mailto_url
        self.enable_net_access = enable_net_access
        self.config_info = cfg

        self.config_info.read_config()

        self.appsdomainstr = self.config_info.appsdomain
        if( len( self.appsdomainstr ) > 0 ):
            self.appsdomainstr = "a/" + self.appsdomainstr + "/"

        self.mail_dict = self.mailto2dict( )


    def append_url( self, tourl, urltag, maildict, dicttag ):
        """ Given a GMail URL underconstruction and the URL tag for the
        current mailto dicttag, add the parameter to the URL """

        if dicttag in maildict:
            tourl = tourl + "&" + urltag + "=" + \
                urllib.quote_plus( maildict[dicttag][0] )

        return( tourl )


    def mailto2dict( self ):
        """ Convert a mailto: reference to a dictionary containing the 
        message parts """
        # get the path string from the 'possible' mailto url
        usplit = urlparse.urlsplit( self.mailto_url, "mailto" )

        path = usplit.path

        try:
            # for some reason, urlsplit is not splitting off the 
            # query string.
            # do it here
            # ( address, qs ) = string.split( path, "?", 1 )
            ( address, query_string ) = path.split( "?", 1 )
        except ValueError:
            address = path

            query_string = usplit.query

        qsdict = urlparse.parse_qs( query_string )

        qsdict['to'] = [ address ]

        if 'attachment' in qsdict:
            qsdict['attach'] = qsdict['attachment']

        outdict = {}
        for (key, value) in qsdict.iteritems():
            for i in range(0, len(value)):
                if key.lower() in [ 'to', 'cc', 'bcc' ]:
                    value[i] = urllib.unquote( value[i]  )
                else:
                    value[i] = urllib.unquote_plus( value[i]  )


            outdict[ key.lower() ] = value

        return( outdict )

    def standard_gmail_url( self ):
        """ If there is no attachment reference, create a direct GMail 
        URL which will create the message """

        dct = self.mail_dict

        tourl = "https://mail.google.com/%smail?view=cm&tf=0&fs=1" % \
            self.appsdomainstr

        tourl = self.append_url( tourl, "to", dct, "to" )
        tourl = self.append_url( tourl, "su", dct, "subject" )
        tourl = self.append_url( tourl, "body", dct, "body" )
        tourl = self.append_url( tourl, "cc", dct, "cc" )
        tourl = self.append_url( tourl, "bcc", dct, "bcc" )

        return( tourl )

    def simple_gmail_url( self ):
        """ url to use if there is no mailto url """

        return( "https://mail.google.com/%s" % self.appsdomainstr )

    def has_attachment( self ):
        """ does the mailto specify a file to be attached? """

        return( 'attach' in self.mail_dict )

    def imap_gmail_url( self ):
        """ if the mailto refers to an attachment, 
        use IMAP to upload the file """

        imap_url = "https://mail.google.com/%smail/#drafts/" % self.appsdomainstr

        if not self.enable_net_access:
            return( imap_url )

        try:
            gm_imap = GMailIMAP( self.mail_dict )
        except:
            GGError( _("Error creating message with attachment") )

        # get the configuration information from GConf
        self.config_info.read_config()

        # if we have a full set of configuration information, 
        # go ahead and try the transfer
        if( self.config_info.info_complete() ):
            try:
                msg_id = gm_imap.send_mail( self.config_info.user, \
                    self.config_info.password )
            except imaplib.IMAP4.error as inst:
                # don't report this failure. 
                # We'll try again with prompted parameters
                pass
            else:
                return( imap_url + msg_id )

        # query for needed information
        if( self.config_info.query_config_info() ):
            # do the IMAP upload
            try:
                msg_id = gm_imap.send_mail( self.config_info.user, \
                    self.config_info.password )
            except imaplib.IMAP4.error as inst:
                error_str = None
                if inst.args[0].find( _("nvalid credentials") ) > 0:
                    error_str = _("Invalid GMail User Name or Password")
                elif inst.args[0].find( _("not enabled for IMAP") ) > 0:
                    error_str = _("You must 'Enable IMAP' in Gmail in order to send attachments")
                else:
                    error_str = inst.args[0]

                raise GGError( error_str )

            #self.config_info.write_config()
        else:
            raise GGError( _("GMail credentials incomplete") )

        return( imap_url + msg_id )

    def gmail_url( self ):
        """ Return a GMail URL appropriate for the mailto handled 
        by this instance """
        if( len( self.mailto_url ) == 0 ):
            gmailurl = self.simple_gmail_url() 
        elif self.has_attachment():
            gmailurl = self.imap_gmail_url()
        else:
            gmailurl = self.standard_gmail_url( )

        return( gmailurl )


def main( ):
    """ given an optional parameter of a valid mailto url, open an appropriate
    gmail web page """

    if( len( sys.argv ) > 1 ):
        mailto = sys.argv[1]
    else:
        mailto = ""

    # set this app as the GNOME mailto handler, new-style
    [ app.set_as_default_for_type( "x-scheme-handler/mailto" ) 
      for app in gio.app_info_get_all_for_type( "x-scheme-handler/mailto" )
      if app.get_id() == "gnome-gmail.desktop" ]

    # quiet mode, to set preferred app in postinstall
    if( len( sys.argv ) > 1 and sys.argv[1] == "-q" ):
        sys.exit(0)

    cfg = ConfigInfo()
    cfg.read_config()
    cfg.write_config()

    #import dbus.glib
    #loop = dbus.mainloop.glib.DBusGMainLoop()
    #sessionbus = dbus.SessionBus( mainloop = loop )
    sessionbus = dbus.SessionBus( )
    notifications_object = sessionbus.get_object(
        'org.freedesktop.Notifications',
        '/org/freedesktop/Notifications')
    interface = dbus.Interface(
        notifications_object,
        'org.freedesktop.Notifications')

    # for reverse compatibility, also set the preferred app the old way
    cfg.query_set_preferred()

    try:
        gm_url = GMailURL( mailto, cfg )
        gmailurl = gm_url.gmail_url()
    except GGError as gerr:
        interface.Notify( "gnome-gmail", 0, '', "gnome-gmail", \
            gerr.value, [], {}, 3000 )
    else:
        webbrowser.open_new_tab( gmailurl )

if __name__ == "__main__":
    main()



