source: trunk/email2trac.py.in @ 525

Last change on this file since 525 was 525, checked in by bas, 13 years ago

fixed some logging errors in email_to_unicode function

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 79.3 KB
RevLine 
[22]1#!@PYTHON@
2# Copyright (C) 2002
3#
4# This file is part of the email2trac utils
5#
6# This program is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any
9# later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
19#
[80]20# For vi/emacs or other use tabstop=4 (vi: set ts=4)
21#
[22]22"""
[54]23email2trac.py -- Email tickets to Trac.
[22]24
25A simple MTA filter to create Trac tickets from inbound emails.
26
27Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28Copyright 2005, Edgewall Software
29
[282]30Authors:
31  Bas van der Vlies <basv@sara.nl>
32  Walter de Jong <walter@sara.nl>
[22]33
34The scripts reads emails from stdin and inserts directly into a Trac database.
35
36How to use
37----------
[282]38 * See https://subtrac.sara.nl/oss/email2trac/
39
[22]40 * Create an config file:
[282]41    [DEFAULT]                        # REQUIRED
42    project      : /data/trac/test   # REQUIRED
43    debug        : 1                 # OPTIONAL, if set print some DEBUG info
[22]44
[282]45    [jouvin]                         # OPTIONAL project declaration, if set both fields necessary
46    project      : /data/trac/jouvin # use -p|--project jouvin. 
[22]47       
48 * default config file is : /etc/email2trac.conf
49
50 * Commandline opions:
[205]51                -h,--help
[507]52                -d, --debug
[205]53                -f,--file  <configuration file>
[410]54                -n,--dry-run
[205]55                -p, --project <project name>
[415]56                -t, --ticket_prefix <name>
[22]57
58SVN Info:
59        $Id: email2trac.py.in 525 2011-06-22 10:44:11Z bas $
60"""
61import os
62import sys
63import string
64import getopt
65import time
66import email
[136]67import email.Iterators
68import email.Header
[22]69import re
70import urllib
71import unicodedata
72import mimetypes
[96]73import traceback
[403]74import logging
75import logging.handlers
[404]76import UserDict
[498]77import tempfile
[433]78
[520]79from datetime import timedelta, datetime
[433]80from stat import *
[22]81
[403]82
[363]83from trac import __version__ as trac_version
[199]84from trac import config as trac_config
[91]85
[508]86## Some global variables
[96]87#
88m = None
[22]89
[508]90# This is to for the function AlwaysNotifyReporter
91sender_email = None
92
[404]93class SaraDict(UserDict.UserDict):
[507]94    def __init__(self, dictin = None):
95        UserDict.UserDict.__init__(self)
96        self.name = None
97       
98        if dictin:
99            if dictin.has_key('name'):
100                self.name = dictin['name']
101                del dictin['name']
102            self.data = dictin
103           
104    def get_value(self, name):
105        if self.has_key(name):
106            return self[name]
107        else:
108            return None
109               
110    def __repr__(self):
111        return repr(self.data)
[182]112
[507]113    def __str__(self):
114        return str(self.data)
115           
116    def __getattr__(self, name):
117        """
118        override the class attribute get method. Return the value
119        from the dictionary
120        """
121        if self.data.has_key(name):
122            return self.data[name]
123        else:
124            return None
125           
126    def __setattr__(self, name, value):
127        """
128        override the class attribute set method only when the UserDict
129        has set its class attribute
130        """
131        if self.__dict__.has_key('data'):
132            self.data[name] = value
133        else:
134            self.__dict__[name] = value
[404]135
[507]136    def __iter__(self):
137        return iter(self.data.keys())
[404]138
[22]139class TicketEmailParser(object):
[507]140    env = None
141    comment = '> '
[359]142
[507]143    def __init__(self, env, parameters, logger, version):
144        self.env = env
[22]145
[507]146        # Database connection
147        #
148        self.db = None
[22]149
[507]150        # Save parameters
151        #
152        self.parameters = parameters
153        self.logger = logger
[206]154
[507]155        # Some useful mail constants
156        #
157        self.email_name = None
158        self.email_addr = None
159        self.email_from = None
160        self.author     = None
161        self.id         = None
162       
163        self.STRIP_CONTENT_TYPES = list()
[72]164
[507]165        ## fields properties via body_text
166        #
167        self.properties = dict()
[452]168
[507]169        self.VERSION = version
[402]170
[507]171        self.get_config = self.env.config.get
[404]172
[507]173        ## init function ##
174        #
175        self.setup_parameters()
[402]176
[507]177    def setup_parameters(self):
178        if self.parameters.umask:
179            os.umask(self.parameters.umask)
[402]180
[507]181        if not self.parameters.spam_level:
182            self.parameters.spam_level = 0
[22]183
[507]184        if not self.parameters.spam_header:
185            self.parameters.spam_header = 'X-Spam-Score'
[207]186
[507]187        if not self.parameters.email_quote:
188            self.parameters.email_quote = '> '
[22]189
[507]190        if not self.parameters.ticket_update_by_subject_lookback:
191            self.parameters.ticket_update_by_subject_lookback = 30
[360]192
[507]193        if self.parameters.verbatim_format == None:
194            self.parameters.verbatim_format = 1
[118]195
[507]196        if self.parameters.reflow == None:
197            self.parameters.reflow = 1
[231]198
[507]199        if self.parameters.binhex:
200            self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
[238]201
[507]202        if self.parameters.applesingle:
203            self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]204
[507]205        if self.parameters.appledouble:
206            self.STRIP_CONTENT_TYPES.append('application/applefile')
[238]207
[507]208        if self.parameters.strip_content_types:
209            items = self.parameters.strip_content_types.split(',')
210            for item in items:
211                self.STRIP_CONTENT_TYPES.append(item.strip())
[294]212
[507]213        if self.parameters.tmpdir:
214            self.parameters.tmpdir = os.path.normcase(str(self.parameters['tmpdir']))
215        else:
216            self.parameters.tmpdir = os.path.normcase('/tmp')
[257]217
[507]218        if self.parameters.email_triggers_workflow == None:
219            self.parameters.email_triggers_workflow = 1
[191]220
[507]221        if not self.parameters.subject_field_separator:
222            self.parameters.subject_field_separator = '&'
223        else:
224            self.parameters.subject_field_separator = self.parameters.subject_field_separator.strip()
[297]225
[507]226        self.trac_smtp_from = self.get_config('notification', 'smtp_from')
227        self.smtp_default_domain = self.get_config('notification', 'smtp_default_domain')
[305]228
[484]229
[507]230        self.system = None
[359]231
[341]232########## Email Header Functions ###########################################################
[339]233
[507]234    def spam(self, message):
235        """
236        # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
237        # Note if Spam_level then '*' are included
238        """
239        spam = False
240        if message.has_key(self.parameters.spam_header):
241            spam_l = string.split(message[self.parameters.spam_header])
[22]242
[507]243            try:
244                number = spam_l[0].count('*')
245            except IndexError, detail:
246                number = 0
247               
248            if number >= self.parameters.spam_level:
249                spam = True
250               
251        # treat virus mails as spam
252        #
253        elif message.has_key('X-Virus-found'):         
254            spam = True
[194]255
[507]256        # How to handle SPAM messages
257        #
258        if self.parameters.drop_spam and spam:
[194]259
[507]260            self.logger.info('Message is a SPAM. Automatic ticket insertion refused (SPAM level > %d)' %self.parameters.spam_level)
261            return 'drop'   
[194]262
[507]263        elif spam:
[194]264
[507]265            return 'Spam'   
266        else:
[22]267
[507]268            return False
[191]269
[507]270    def email_header_acl(self, keyword, header_field, default):
271        """
272        This function wil check if the email address is allowed or denied
273        to send mail to the ticket list
274        """
275        self.logger.debug('function email_header_acl: %s' %keyword)
[409]276
[507]277        try:
278            mail_addresses = self.parameters[keyword]
[221]279
[507]280            # Check if we have an empty string
281            #
282            if not mail_addresses:
283                return default
[221]284
[507]285        except KeyError, detail:
286            self.logger.debug('%s not defined, all messages are allowed.' %(keyword))
[206]287
[507]288            return default
[206]289
[507]290        mail_addresses = string.split(mail_addresses, ',')
[221]291
[507]292        for entry in mail_addresses:
293            entry = entry.strip()
294            TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
295            result =  TO_RE.search(header_field)
296            if result:
297                return True
[149]298
[507]299        return False
[208]300
[507]301    def email_header_txt(self, m):
302        """
303        Display To and CC addresses in description field
304        """
305        s = ''
[334]306
[507]307        if m['To'] and len(m['To']) > 0:
308            s = "'''To:''' %s\r\n" %(m['To'])
309        if m['Cc'] and len(m['Cc']) > 0:
310            s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
[22]311
[507]312        return  self.email_to_unicode(s)
[22]313
[138]314
[507]315    def get_sender_info(self, message):
316        """
317        Get the default author name and email address from the message
318        """
[43]319
[507]320        self.email_to = self.email_to_unicode(message['to'])
321        self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
[226]322
[507]323        self.email_from = self.email_to_unicode(message['from'])
324        self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
[142]325
[507]326        ## Trac can not handle author's name that contains spaces
327        #
[518]328        if self.email_addr.lower() == self.trac_smtp_from.lower():
[507]329            if self.email_name:
330                self.author = self.email_name
331            else:
332                self.author = "email2trac"
333        else:
334            self.author = self.email_addr
[304]335
[507]336        if self.parameters.ignore_trac_user_settings:
337            return
[194]338
[507]339        # Is this a registered user, use email address as search key:
340        # result:
341        #   u : login name
342        #   n : Name that the user has set in the settings tab
343        #   e : email address that the user has set in the settings tab
344        #
345        users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
346            if (
347                (e and (e.lower() == self.email_addr.lower())) or
[518]348                (u.lower() + '@' + self.smtp_default_domain.lower() == self.email_addr.lower())
[507]349            )
350            ]
[43]351
[507]352        if len(users) >= 1:
353            self.email_from = users[0][0]
354            self.author = users[0][0]
[45]355
[507]356    def set_reply_fields(self, ticket, message):
357        """
358        Set all the right fields for a new ticket
359        """
360        self.logger.debug('function set_reply_fields')
[72]361
[507]362        ## Only use name or email adress
363        #ticket['reporter'] = self.email_from
364        ticket['reporter'] = self.author
[270]365
366
[507]367        # Put all CC-addresses in ticket CC field
368        #
369        if self.parameters.reply_all:
[43]370
[507]371            email_cc = ''
[299]372
[507]373            cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
[299]374
[507]375            if not cc_addrs:
376                return
[43]377
[507]378            ## Build a list of forbidden CC addresses
379            #
380            #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
381            #to_list = list()
382            #for n,e in to_addrs:
383            #   to_list.append(e)
384               
385            # Always Remove reporter email address from cc-list
386            #
387            try:
388                cc_addrs.remove((self.author, self.email_addr))
389            except ValueError, detail:
390                pass
[43]391
[507]392            for name,addr in cc_addrs:
393       
394                ## Prevent mail loop
395                #
396                #if addr in to_list:
[304]397
[507]398                if addr == self.trac_smtp_from:
399                    self.logger.debug("Skipping %s mail address for CC-field" %(addr))
400                    continue
[43]401
[507]402                if email_cc:
403                    email_cc = '%s, %s' %(email_cc, addr)
404                else:
405                    email_cc = addr
[96]406
[507]407            if email_cc:
408                self.logger.debug('set_reply_fields: %s' %email_cc)
[299]409
[507]410                ticket['cc'] = self.email_to_unicode(email_cc)
[299]411
[339]412
413########## DEBUG functions  ###########################################################
414
[507]415    def debug_body(self, message_body, temporary_file=False):
416        if temporary_file:
[514]417            (fd, body_file) = tempfile.mkstemp('.email2trac')
418            fx = os.fdopen(fd, 'wb')
[507]419        else:
420            body_file = os.path.join(self.parameters.tmpdir, 'body.txt')
[514]421            fx = open(body_file, 'wb')
[310]422
[507]423        if self.parameters.dry_run:
424            print 'DRY-RUN: not saving body to %s' %(body_file)
425            return
[331]426
[507]427        print 'writing body to %s' %(body_file)
428        if not message_body:
429                message_body = '(None)'
[310]430
[507]431        message_body = message_body.encode('utf-8')
432        #message_body = unicode(message_body, 'iso-8859-15')
[310]433
[507]434        fx.write(message_body)
435        fx.close()
436        try:
437            os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
438        except OSError:
439            pass
[310]440
[507]441    def debug_attachments(self, message_parts):
442        """
443        """
444        self.logger.debug('function debug_attachments')
445       
446        n = 0
447        for item in message_parts:
448            # Skip inline text parts
449            if not isinstance(item, tuple):
450                continue
451               
452            (original, filename, part) = item
[310]453
[507]454            n = n + 1
455            print 'part%d: Content-Type: %s' % (n, part.get_content_type())
456       
457            s = 'part%d: filename: %s' %(n, filename)
458            self.print_unicode(s)
459   
460            ## Forbidden chars
461            #
462            filename = filename.replace('\\', '_')
463            filename = filename.replace('/', '_')
464   
[348]465
[507]466            part_file = os.path.join(self.parameters.tmpdir, filename)
467            s = 'writing part%d (%s)' % (n,part_file)
468            self.print_unicode(s)
[331]469
[507]470            if self.parameters.dry_run:
471                print 'DRY_RUN: NOT saving attachments'
472                continue
[331]473
[507]474            part_file = util.text.unicode_quote(part_file)
[377]475
[507]476            fx = open(part_file, 'wb')
477            text = part.get_payload(decode=1)
[331]478
[507]479            if not text:
480                text = '(None)'
[331]481
[507]482            fx.write(text)
483            fx.close()
[331]484
[507]485            try:
486                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
487            except OSError:
488                pass
[310]489
[507]490    def save_email_for_debug(self, message, create_tempfile=False):
[309]491
[507]492        if create_tempfile:
[514]493            (fd, msg_file) = tempfile.mkstemp('.email2trac')
494            fx = os.fdopen(fd, 'wb')
[507]495        else:
496            #msg_file = '/var/tmp/msg.txt'
497            msg_file = os.path.join(self.parameters.tmpdir, 'msg.txt')
[514]498            fx = open(msg_file, 'wb')
[173]499
[507]500        if self.parameters.dry_run:
501            print 'DRY_RUN: NOT saving email message to %s' %(msg_file)
502        else:
503            print 'saving email to %s' %(msg_file)
[44]504
[507]505            fx.write('%s' % message)
506            fx.close()
507           
508            try:
509                os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
510            except OSError:
511                pass
[331]512
[507]513        message_parts = self.get_message_parts(message)
514        message_parts = self.unique_attachment_names(message_parts)
515        body_text = self.get_body_text(message_parts)
516        self.debug_body(body_text, True)
517        self.debug_attachments(message_parts)
[309]518
[339]519########## Conversion functions  ###########################################################
520
[507]521    def email_to_unicode(self, message_str):
522        """
523        Email has 7 bit ASCII code, convert it to unicode with the charset
524        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
525        understands it.
526        """
527        self.logger.debug("function email_to_unicode")
[341]528
[507]529        results =  email.Header.decode_header(message_str)
[341]530
[507]531        s = None
532        for text,format in results:
533            if format:
534                try:
535                    temp = unicode(text, format)
536                except UnicodeError, detail:
537                    # This always works
538                    #
[525]539                    msg = 'ERROR: Could not find charset: %s, please install' %format
540                    self.logger.error(msg)
[507]541                    temp = unicode(text, 'iso-8859-15')
542                except LookupError, detail:
[514]543                    msg = 'ERROR: Could not find charset: %s, please install' %format
[525]544                    self.logger.error(msg)
[507]545                    #temp = unicode(text, 'iso-8859-15')
546                    temp = message_str
547                       
548            else:
549                temp = string.strip(text)
550                temp = unicode(text, 'iso-8859-15')
[341]551
[507]552            if s:
553                s = '%s %s' %(s, temp)
554            else:
555                s = '%s' %temp
[341]556
[507]557        #s = s.encode('utf-8')
558        return s
[341]559
[507]560    def str_to_dict(self, s):
561        """
562        Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
563        """
564        self.logger.debug("function str_to_dict")
[164]565
[507]566        fields = string.split(s, self.parameters.subject_field_separator)
[262]567
[507]568        result = dict()
569        for field in fields:
570            try:
571                index, value = string.split(field, '=')
[169]572
[507]573                # We can not change the description of a ticket via the subject
574                # line. The description is the body of the email
575                #
576                if index.lower() in ['description']:
577                    continue
[169]578
[507]579                if value:
580                    result[index.lower()] = value
[169]581
[507]582            except ValueError:
583                pass
584        return result
[167]585
[507]586    def print_unicode(self,s):
587        """
588        This function prints unicode strings if possible else it will quote it
589        """
590        try:
591            self.logger.debug(s)
592        except UnicodeEncodeError, detail:
593            self.logger.debug(util.text.unicode_quote(s))
[379]594
[503]595
[507]596    def html_2_txt(self, data):
597        """
598        Various routines to convert html syntax to valid trac wiki syntax
599        """
600        self.logger.debug('function html_2_txt')
[503]601
[507]602        ## This routine make an safe html that can be include
603        #  in trac, but no further text processing can be done
604        #
605#       try:
606#           from lxml.html.clean import Cleaner
607#           tags_rm = list()
608#           tags_rm.append('body')
[503]609#
[507]610#           cleaner = Cleaner(remove_tags=tags_rm )
611#           parsed_data = cleaner.clean_html(data)
612#           parsed_data = '\n{{{\n#!html\n' + parsed_data + '\n}}}\n'
[503]613#
[507]614#           return parsed_data
615#           
616#       except ImportError::
617#           pass
[503]618
[507]619        parsed_data = None
620        if self.parameters.html2text_cmd:
[514]621            (fd, tmp_file) = tempfile.mkstemp('email2trac.html')
622            f = os.fdopen(fd, 'w')
623
[507]624            cmd = '%s %s' %(self.parameters.html2text_cmd, tmp_file)
625            self.logger.debug('\t html2text conversion %s'%(cmd))
626   
627            if self.parameters.dry_run:
628                print 'DRY_RUN: html2text conversion command: %s\n' %(cmd)
[503]629
[507]630            else:
631                f.write(data)
632                f.close()
[503]633
[507]634                lines = os.popen(cmd).readlines()
635                parsed_data =  ''.join(lines)
[503]636
[507]637                os.unlink(tmp_file)
[503]638
[507]639        else:
640            self.logger.debug('\t No html2text conversion tool specified in email2trac.conf')
[503]641
[507]642        return parsed_data
[503]643
[339]644########## TRAC ticket functions  ###########################################################
645
[507]646    def check_permission_participants(self, tkt, action):
647        """
648        Check if the mailer is allowed to update the ticket
649        """
650        self.logger.debug('function check_permission_participants')
[398]651
[518]652        if tkt['reporter'].lower() in [self.author.lower(), self.email_addr.lower()]:
[507]653            self.logger.debug('ALLOW, %s is the ticket reporter' %(self.email_addr))
[408]654
[507]655            return True
[398]656
[507]657        perm = PermissionSystem(self.env)
658        if perm.check_permission(action, self.author):
659            self.logger.debug('ALLOW, %s has trac permission to update the ticket' %(self.author))
[408]660
[507]661            return True
662       
663        # Is the updater in the CC?
664        try:
665            cc_list = tkt['cc'].split(',')
666            for cc in cc_list:
[518]667                if self.email_addr.lower() in cc.lower.strip():
[507]668                    self.logger.debug('ALLOW, %s is in the CC' %(self.email_addr))
[398]669
[507]670                    return True
[398]671
[507]672        except KeyError:
673            pass
[398]674
[507]675        return False
[474]676
[507]677    def check_permission(self, tkt, action):
678        """
679        check if the reporter has the right permission for the action:
[397]680          - TICKET_CREATE
681          - TICKET_MODIFY
[474]682          - TICKET_APPEND
683          - TICKET_CHGPROP
[397]684
[507]685        There are three models:
686            - None      : no checking at all
687            - trac      : check the permission via trac permission model
688            - email2trac: ....
689        """
690        self.logger.debug("function check_permission")
[397]691
[507]692        if self.parameters.ticket_permission_system in ['trac']:
[398]693
[507]694            perm = PermissionSystem(self.env)
695            if perm.check_permission(action, self.author):
696                return True
697            else:
698                return False
[398]699
[507]700        elif self.parameters.ticket_permission_system in ['update_restricted_to_participants']:
701            return (self.check_permission_participants(tkt, action))   
[398]702
[507]703        ## Default is to allow everybody ticket updates and ticket creation
704        #
705        else:
706                return True
[397]707
708
[507]709    def update_ticket_fields(self, ticket, user_dict, new=None):
710        """
711        This will update the ticket fields. It will check if the
712        given fields are known and if the right values are specified
713        It will only update the ticket field value:
714            - If the field is known
715            - If the value supplied is valid for the ticket field.
716              If not then there are two options:
717               1) Skip the value (new=None)
718               2) Set default value for field (new=1)
719        """
720        self.logger.debug("function update_ticket_fields")
[169]721
[507]722        ## Check only permission model on ticket updates
723        #
724        if not new:
725            if self.parameters.ticket_permission_system:
726                if not self.check_permission(ticket, 'TICKET_CHGPROP'):
727                    self.logger.info('Reporter: %s has no permission to change ticket properties' %self.author)
728                    return False
[474]729
[507]730        ## Build a system dictionary from the ticket fields
731        #  with field as index and option as value
732        #
733        sys_dict = dict()
734        for field in ticket.fields:
735            try:
736                sys_dict[field['name']] = field['options']
[169]737
[507]738            except KeyError:
739                #sys_dict[field['name']] = None
740                pass
[169]741
[507]742        ## Check user supplied fields an compare them with the
743        #  system one's
744        #
745        for field,value in user_dict.items():
[524]746
[507]747            if self.parameters.debug:
748                s = 'user_field\t %s = %s' %(field,value)
749                self.print_unicode(s)
[169]750
[524]751            if not field in ticket.fields: 
752                self.logger.debug('%s is not a valid field for tickets' %(field))
753                continue
754
[507]755            ## To prevent mail loop
756            #
757            if field == 'cc':
[301]758
[507]759                cc_list = user_dict['cc'].split(',')
[301]760
[507]761                if self.trac_smtp_from in cc_list:
762                    self.logger.debug('MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from))
[408]763
[507]764                    cc_list.remove(self.trac_smtp_from)
[301]765
[507]766                value = ','.join(cc_list)
767               
[301]768
[507]769            ## Check if every value is allowed for this field
770            #
771            if sys_dict.has_key(field):
[169]772
[507]773                if value in sys_dict[field]:
774                    ticket[field] = value
775                else:
776                    ## Must we set a default if value is not allowed
777                    #
778                    if new:
779                        value = self.get_config('ticket', 'default_%s' %(field) )
[169]780
[507]781            else:
782                ## Only set if we have a value
783                #
784                #if value:
785                #   ticket[field] = value
786                ticket[field] = value
[202]787
[507]788            if self.parameters.debug:
789                s = 'ticket_field\t %s = %s' %(field,  ticket[field])
790                self.print_unicode(s)
[345]791
[507]792    def ticket_update(self, m, id, spam):
793        """
794        If the current email is a reply to an existing ticket, this function
795        will append the contents of this email to that ticket, instead of
796        creating a new one.
797        """
798        self.logger.debug("function ticket_update")
[202]799
[507]800        if not self.parameters.ticket_update:
801            self.logger.debug("ticket_update disabled")
802            return False
[456]803
[507]804        ## Must we update ticket fields
805        #
806        update_fields = dict()
807        try:
808            id, keywords = string.split(id, '?')
[262]809
[507]810            update_fields = self.str_to_dict(keywords)
[165]811
[507]812            ## Strip '#'
813            #
814            self.id = int(id[1:])
[165]815
[507]816        except ValueError:
[390]817
[507]818            ## Strip '#'
819            #
820            self.id = int(id[1:])
[164]821
[507]822        self.logger.debug("ticket_update id %s" %id)
[71]823
[507]824        ## When is the change committed
825        #
826        if self.VERSION < 0.11:
827            when = int(time.time())
828        else:
829            when = datetime.now(util.datefmt.utc)
[77]830
[507]831        try:
832            tkt = Ticket(self.env, self.id, self.db)
[390]833
[507]834        except util.TracError, detail:
[390]835
[507]836            ## Not a valid ticket
837            #
838            self.id = None
839            return False
[126]840
[507]841        ## Check the permission of the reporter
842        #
843        if self.parameters.ticket_permission_system:
844            if not self.check_permission(tkt, 'TICKET_APPEND'):
845                self.logger.info('Reporter: %s has no permission to add comments or attachments to tickets' %self.author)
846                return False
[398]847
[507]848        ## How many changes has this ticket
849        #
850        # cnum = len(tkt.get_changelog())
851        grouped = TicketModule(self.env).grouped_changelog_entries(tkt, self.db)
852        cnum = sum(1 for e in grouped) + 1
[288]853
[486]854
[507]855        ## reopen the ticket if it is was closed
856        #  We must use the ticket workflow framework
857        #
858        if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
[220]859
[507]860            self.logger.debug('Workflow ticket update fields: ')
[257]861
[507]862            from trac.ticket.default_workflow import ConfigurableTicketWorkflow
863            from trac.test import Mock, MockPerm
[468]864
[507]865            req = Mock(authname=self.author, perm=MockPerm(), args={})
866            try:
867                workflow = self.parameters['workflow_%s' %tkt['status']]
868            except KeyError:
869                ## fallback for compability (Will be deprecated)
870                #
871                if tkt['status'] in ['closed']:
872                    workflow = self.parameters.workflow
873                else:   
874                    workflow = None
[468]875
[507]876            controller = ConfigurableTicketWorkflow(self.env)
877            #print controller.actions
878            #print controller.actions.keys()
879            #print controller.get_ticket_actions(req, tkt)
880            #print controller.actions[workflow]
881            #print controller.actions[workflow]['permissions'] is a list
[469]882
[507]883            if workflow:
[257]884
[507]885                if self.parameters.ticket_permission_system:
[257]886
[507]887                    if self.check_permission(tkt, controller.actions[workflow]['permissions'][0]):
888                        fields = controller.get_ticket_changes(req, tkt, workflow)
889                    else:
890                        fields = dict()
891                        self.logger.info('Reporter: %s has no permission to trigger workflow' %self.author)
[257]892
[507]893                else:
894                    fields = controller.get_ticket_changes(req, tkt, workflow)
[257]895
[507]896                for key in fields.keys():
897                    self.logger.debug('\t %s : %s' %(key, fields[key]))
898                    tkt[key] = fields[key]
[257]899
[507]900        ## Old pre 0.11 situation
901        #
902        elif self.parameters.email_triggers_workflow:
[470]903
[507]904            self.logger.debug('email triggers workflow pre trac 0.11')
[470]905
[507]906            if tkt['status'] in ['closed']:
907                tkt['status'] = 'reopened'
908                tkt['resolution'] = ''
[257]909
[507]910        else:
911            self.logger.debug('email triggers workflow disabled')
[469]912
[507]913        ## Must we update some ticket fields properties via subject line
914        #
915        if update_fields:
916            self.update_ticket_fields(tkt, update_fields)
[166]917
[507]918        message_parts = self.get_message_parts(m)
919        message_parts = self.unique_attachment_names(message_parts)
[210]920
[507]921        ## Must we update some ticket fields properties via body_text
922        #
923        if self.properties:
924                self.update_ticket_fields(tkt, self.properties)
[309]925
[507]926        if self.parameters.email_header:
927            message_parts.insert(0, self.email_header_txt(m))
[76]928
[507]929        body_text = self.get_body_text(message_parts)
[236]930
[507]931        error_with_attachments = self.attach_attachments(message_parts)
[348]932
[507]933        if body_text.strip() or update_fields or self.properties:
934            if self.parameters.dry_run:
935                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
936            else:
937                if error_with_attachments:
938                    body_text = '%s\\%s' %(error_with_attachments, body_text)
939                self.logger.debug('tkt.save_changes(%s, %d)' %(self.author, cnum))
940                tkt.save_changes(self.author, body_text, when, None, str(cnum))
941           
[219]942
[507]943        if not spam:
944            self.notify(tkt, False, when)
[72]945
[507]946        return True
[71]947
[507]948    def set_ticket_fields(self, ticket):
949        """
950        set the ticket fields to value specified
951            - /etc/email2trac.conf with <prefix>_<field>
952            - trac default values, trac.ini
953        """
954        self.logger.debug('function set_ticket_fields')
[410]955
[507]956        user_dict = dict()
[202]957
[507]958        for field in ticket.fields:
[202]959
[507]960            name = field['name']
[202]961
[507]962            ## default trac value
963            #
964            if not field.get('custom'):
965                value = self.get_config('ticket', 'default_%s' %(name) )
966                if (name in ['resolution']) and (value in ['fixed']):
967                    value = None
968            else:
969                ##  Else get the default value for reporter
970                #
971                value = field.get('value')
972                options = field.get('options')
[345]973
[507]974                if value and options and (value not in options):
975                     value = options[int(value)]
976   
977            if self.parameters.debug:
978                s = 'trac[%s] = %s' %(name, value)
979                self.print_unicode(s)
[202]980
[507]981            ## email2trac.conf settings
982            #
983            prefix = self.parameters.ticket_prefix
984            try:
985                value = self.parameters['%s_%s' %(prefix, name)]
986                if self.parameters.debug:
987                    s = 'email2trac[%s] = %s ' %(name, value)
988                    self.print_unicode(s)
[202]989
[507]990            except KeyError, detail:
991                pass
992       
993            if value:
994                user_dict[name] = value
995                if self.parameters.debug:
996                    s = 'used %s = %s' %(name, value)
997                    self.print_unicode(s)
[202]998
[507]999        self.update_ticket_fields(ticket, user_dict, new=1)
[202]1000
[507]1001        if 'status' not in user_dict.keys():
1002            ticket['status'] = 'new'
[202]1003
1004
[507]1005    def ticket_update_by_subject(self, subject):
1006        """
1007        This list of Re: prefixes is probably incomplete. Taken from
1008        wikipedia. Here is how the subject is matched
1009          - Re: <subject>
1010          - Re: (<Mail list label>:)+ <subject>
[202]1011
[507]1012        So we must have the last column
1013        """
1014        self.logger.debug('function ticket_update_by_subject')
[356]1015
[507]1016        matched_id = None
1017        if self.parameters.ticket_update and self.parameters.ticket_update_by_subject:
1018               
1019            SUBJECT_RE = re.compile(r'^(?:(?:RE|AW|VS|SV|FW|FWD):\s*)+(.*)', re.IGNORECASE)
1020            result = SUBJECT_RE.search(subject)
[356]1021
[507]1022            if result:
1023                ## This is a reply
1024                #
1025                orig_subject = result.group(1)
[356]1026
[507]1027                self.logger.debug('subject search string: %s' %(orig_subject))
[356]1028
[507]1029                cursor = self.db.cursor()
1030                summaries = [orig_subject, '%%: %s' % orig_subject]
[356]1031
[520]1032                ## Time resolution is in micoseconds
[507]1033                #
[520]1034                search_date = datetime.now(util.datefmt.utc) - timedelta(days=self.parameters.ticket_update_by_subject_lookback)
1035                lookback = util.datefmt.to_utimestamp(search_date)
[356]1036
[507]1037                for summary in summaries:
1038                    self.logger.debug('Looking for summary matching: "%s"' % summary)
[408]1039
[507]1040                    sql = """SELECT id FROM ticket
1041                            WHERE changetime >= %s AND summary LIKE %s
1042                            ORDER BY changetime DESC"""
1043                    cursor.execute(sql, [lookback, summary.strip()])
[356]1044
[507]1045                    for row in cursor:
1046                        (matched_id,) = row
[408]1047
[507]1048                        self.logger.debug('Found matching ticket id: %d' % matched_id)
[408]1049
[507]1050                        break
[356]1051
[507]1052                    if matched_id:
1053                        matched_id = '#%d' % matched_id
1054                        return (matched_id, orig_subject)
1055                   
1056                    subject = orig_subject
[356]1057
[507]1058        return (matched_id, subject)
[356]1059
1060
[507]1061    def new_ticket(self, msg, subject, spam, set_fields = None):
1062        """
1063        Create a new ticket
1064        """
1065        self.logger.debug('function new_ticket')
[250]1066
[507]1067        tkt = Ticket(self.env)
[326]1068
[507]1069        self.set_reply_fields(tkt, msg)
[326]1070
[507]1071        self.set_ticket_fields(tkt)
[202]1072
[507]1073        ## Check the permission of the reporter
1074        #
1075        if self.parameters.ticket_permission_system:
1076            if not self.check_permission(tkt, 'TICKET_CREATE'):
1077                self.logger.info('Reporter: %s has no permission to create tickets' %self.author)
1078                return False
[397]1079
[507]1080        ## Old style setting for component, will be removed
1081        #
1082        if spam:
1083            tkt['component'] = 'Spam'
[204]1084
[507]1085        elif self.parameters.has_key('component'):
1086            tkt['component'] = self.parameters['component']
[201]1087
[507]1088        if not msg['Subject']:
1089            tkt['summary'] = u'(No subject)'
1090        else:
1091            tkt['summary'] = subject
[22]1092
1093
[507]1094        if set_fields:
1095            rest, keywords = string.split(set_fields, '?')
[262]1096
[507]1097            if keywords:
1098                update_fields = self.str_to_dict(keywords)
1099                self.update_ticket_fields(tkt, update_fields)
[262]1100
[296]1101
[507]1102        message_parts = self.get_message_parts(msg)
[309]1103
[507]1104        ## Must we update some ticket fields properties via body_text
1105        #
1106        if self.properties:
1107                self.update_ticket_fields(tkt, self.properties)
[309]1108
[507]1109        message_parts = self.unique_attachment_names(message_parts)
1110       
1111        ## produce e-mail like header
1112        #
1113        head = ''
1114        if self.parameters.email_header:
1115            head = self.email_header_txt(msg)
1116            message_parts.insert(0, head)
1117           
1118        body_text = self.get_body_text(message_parts)
[45]1119
[507]1120        tkt['description'] = body_text
[90]1121
[507]1122        ## When is the change committed
1123        #
1124        if self.VERSION < 0.11:
1125            when = int(time.time())
1126        else:
1127            when = datetime.now(util.datefmt.utc)
[45]1128
[507]1129        if self.parameters.dry_run:
1130            print 'DRY_RUN: tkt.insert()'
1131        else:
1132            self.id = tkt.insert()
1133   
1134        changed = False
1135        comment = ''
[77]1136
[507]1137        ## some routines in trac are dependend on ticket id
1138        #  like alternate notify template
1139        #
1140        if self.parameters.alternate_notify_template:
1141            tkt['id'] = self.id
1142            changed = True
[273]1143
[507]1144        ## Rewrite the description if we have mailto enabled
1145        #
1146        if self.parameters.mailto_link:
1147            changed = True
1148            comment = u'\nadded mailto line\n'
1149            mailto = self.html_mailto_link( m['Subject'])
[253]1150
[507]1151            tkt['description'] = u'%s\r\n%s%s\r\n' \
1152                %(head, mailto, body_text)
1153   
1154        ## Save the attachments to the ticket   
1155        #
1156        error_with_attachments =  self.attach_attachments(message_parts)
[295]1157
[507]1158        if error_with_attachments:
1159            changed = True
1160            comment = '%s\n%s\n' %(comment, error_with_attachments)
[45]1161
[507]1162        if changed:
1163            if self.parameters.dry_run:
1164                print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
1165            else:
1166                tkt.save_changes(tkt['reporter'], comment)
1167                #print tkt.get_changelog(self.db, when)
[90]1168
[507]1169        if not spam:
1170            self.notify(tkt, True)
[45]1171
[260]1172
[507]1173    def attach_attachments(self, message_parts, update=False):
1174        '''
1175        save any attachments as files in the ticket's directory
1176        '''
1177        self.logger.debug('function attach_attachments()')
[342]1178
[507]1179        if self.parameters.dry_run:
1180            print "DRY_RUN: no attachments attached to tickets"
1181            return ''
[342]1182
[507]1183        count = 0
[342]1184
[507]1185        ## Get Maxium attachment size
1186        #
1187        max_size = int(self.get_config('attachment', 'max_size'))
1188        status   = None
1189       
1190        for item in message_parts:
1191            ## Skip body parts
1192            #
1193            if not isinstance(item, tuple):
1194                continue
1195               
1196            (original, filename, part) = item
[477]1197
[507]1198            ## We have to determine the size so we use this temporary solution. we must escape it
1199            #  else we get UnicodeErrors.
1200            #
1201            path, fd =  util.create_unique_file(os.path.join(self.parameters.tmpdir, util.text.unicode_quote(filename)))
1202            text = part.get_payload(decode=1)
1203            if not text:
1204                text = '(None)'
1205            fd.write(text)
1206            fd.close()
[342]1207
[507]1208            ## get the file_size
1209            #
1210            stats = os.lstat(path)
1211            file_size = stats[ST_SIZE]
[342]1212
[507]1213            ## Check if the attachment size is allowed
1214            #
1215            if (max_size != -1) and (file_size > max_size):
1216                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1217                    %(status, original, file_size, max_size)
[342]1218
[507]1219                os.unlink(path)
1220                continue
1221            else:
1222                count = count + 1
1223                   
1224            ## Insert the attachment
1225            #
1226            fd = open(path, 'rb')
1227            if self.system == 'discussion':
1228                att = attachment.Attachment(self.env, 'discussion', 'topic/%s'
1229                  % (self.id,))
1230            else:
1231                self.logger.debug('Attach %s to ticket %d' %(util.text.unicode_quote(filename), self.id))
1232                att = attachment.Attachment(self.env, 'ticket', self.id)
[359]1233 
[507]1234            ## This will break the ticket_update system, the body_text is vaporized
1235            #  ;-(
1236            #
1237            if not update:
1238                att.author = self.author
1239                att.description = self.email_to_unicode('Added by email2trac')
[342]1240
[507]1241            try:
1242                self.logger.debug('Insert atachment')
1243                att.insert(filename, fd, file_size)
1244            except OSError, detail:
1245                self.logger.info('%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail))
1246                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
[342]1247
[507]1248            ## Remove the created temporary filename
1249            #
1250            fd.close()
1251            os.unlink(path)
[342]1252
[507]1253        ## return error
1254        #
1255        return status
[342]1256
[359]1257########## Fullblog functions  #################################################
[339]1258
[507]1259    def blog(self, id):
1260        """
1261        The blog create/update function
1262        """
1263        ## import the modules
1264        #
1265        from tracfullblog.core import FullBlogCore
1266        from tracfullblog.model import BlogPost, BlogComment
1267        from trac.test import Mock, MockPerm
[260]1268
[507]1269        ## instantiate blog core
1270        #
1271        blog = FullBlogCore(self.env)
1272        req = Mock(authname='anonymous', perm=MockPerm(), args={})
[312]1273
[507]1274        if id:
[260]1275
[507]1276            ## update blog
1277            #
1278            comment = BlogComment(self.env, id)
1279            comment.author = self.author
[312]1280
[507]1281            message_parts = self.get_message_parts(m)
1282            comment.comment = self.get_body_text(message_parts)
[312]1283
[507]1284            blog.create_comment(req, comment)
[260]1285
[507]1286        else:
1287            ## create blog
1288            #
1289            import time
1290            post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
[260]1291
[507]1292            #post = BlogPost(self.env, blog._get_default_postname(self.env))
1293           
1294            post.author = self.author
1295            post.title = self.email_to_unicode(m['Subject'])
[312]1296
[507]1297            message_parts = self.get_message_parts(m)
1298            post.body = self.get_body_text(message_parts)
1299           
1300            blog.create_post(req, post, self.author, u'Created by email2trac', False)
[260]1301
1302
[359]1303########## Discussion functions  ##############################################
[342]1304
[507]1305    def discussion_topic(self, content, subject):
[342]1306
[507]1307        ## Import modules.
1308        #
1309        from tracdiscussion.api import DiscussionApi
1310        from trac.util.datefmt import to_timestamp, utc
[359]1311
[507]1312        self.logger.debug('Creating a new topic in forum:', self.id)
[359]1313
[507]1314        ## Get dissussion API component.
1315        #
1316        api = self.env[DiscussionApi]
1317        context = self._create_context(content, subject)
[359]1318
[507]1319        ## Get forum for new topic.
1320        #
1321        forum = api.get_forum(context, self.id)
[359]1322
[507]1323        if not forum:
1324            self.logger.error("ERROR: Replied forum doesn't exist")
[359]1325
[507]1326        ## Prepare topic.
1327        #
1328        topic = {'forum' : forum['id'],
1329                 'subject' : context.subject,
1330                 'time': to_timestamp(datetime.now(utc)),
1331                 'author' : self.author,
1332                 'subscribers' : [self.email_addr],
1333                 'body' : self.get_body_text(context.content_parts)}
[359]1334
[507]1335        ## Add topic to DB and commit it.
1336        #
1337        self._add_topic(api, context, topic)
1338        self.db.commit()
[359]1339
[507]1340    def discussion_topic_reply(self, content, subject):
[359]1341
[507]1342        ## Import modules.
1343        #
1344        from tracdiscussion.api import DiscussionApi
1345        from trac.util.datefmt import to_timestamp, utc
[359]1346
[507]1347        self.logger.debug('Replying to discussion topic', self.id)
[359]1348
[507]1349        ## Get dissussion API component.
1350        #
1351        api = self.env[DiscussionApi]
1352        context = self._create_context(content, subject)
[359]1353
[507]1354        ## Get replied topic.
1355        #
1356        topic = api.get_topic(context, self.id)
[359]1357
[507]1358        if not topic:
1359            self.logger.error("ERROR: Replied topic doesn't exist")
[359]1360
[507]1361        ## Prepare message.
1362        #
1363        message = {'forum' : topic['forum'],
1364                   'topic' : topic['id'],
1365                   'replyto' : -1,
1366                   'time' : to_timestamp(datetime.now(utc)),
1367                   'author' : self.author,
1368                   'body' : self.get_body_text(context.content_parts)}
[359]1369
[507]1370        ## Add message to DB and commit it.
1371        #
1372        self._add_message(api, context, message)
1373        self.db.commit()
[359]1374
[507]1375    def discussion_message_reply(self, content, subject):
[359]1376
[507]1377        ## Import modules.
1378        #
1379        from tracdiscussion.api import DiscussionApi
1380        from trac.util.datefmt import to_timestamp, utc
[359]1381
[507]1382        self.logger.debug('Replying to discussion message', self.id)
[359]1383
[507]1384        ## Get dissussion API component.
1385        #
1386        api = self.env[DiscussionApi]
1387        context = self._create_context(content, subject)
[359]1388
[507]1389        ## Get replied message.
1390        #
1391        message = api.get_message(context, self.id)
[359]1392
[507]1393        if not message:
1394            self.logger.error("ERROR: Replied message doesn't exist")
[359]1395
[507]1396        ## Prepare message.
1397        #
1398        message = {'forum' : message['forum'],
1399                   'topic' : message['topic'],
1400                   'replyto' : message['id'],
1401                   'time' : to_timestamp(datetime.now(utc)),
1402                   'author' : self.author,
1403                   'body' : self.get_body_text(context.content_parts)}
[359]1404
[507]1405        ## Add message to DB and commit it.
1406        #
1407        self._add_message(api, context, message)
1408        self.db.commit()
[359]1409
[507]1410    def _create_context(self, content, subject):
[359]1411
[507]1412        ## Import modules.
1413        #
1414        from trac.mimeview import Context
1415        from trac.web.api import Request
1416        from trac.perm import PermissionCache
[359]1417
[507]1418        ## TODO: Read server base URL from config.
1419        #  Create request object to mockup context creation.
1420        #
1421        environ = {'SERVER_PORT' : 80,
1422                   'SERVER_NAME' : 'test',
1423                   'REQUEST_METHOD' : 'POST',
1424                   'wsgi.url_scheme' : 'http',
1425                   'wsgi.input' : sys.stdin}
1426        chrome =  {'links': {},
1427                   'scripts': [],
1428                   'ctxtnav': [],
1429                   'warnings': [],
1430                   'notices': []}
[359]1431
[507]1432        if self.env.base_url_for_redirect:
1433            environ['trac.base_url'] = self.env.base_url
[359]1434
[507]1435        req = Request(environ, None)
1436        req.chrome = chrome
1437        req.tz = 'missing'
1438        req.authname = self.author
1439        req.perm = PermissionCache(self.env, self.author)
1440        req.locale = None
[359]1441
[507]1442        ## Create and return context.
1443        #
1444        context = Context.from_request(req)
1445        context.realm = 'discussion-email2trac'
1446        context.cursor = self.db.cursor()
1447        context.content = content
1448        context.subject = subject
[359]1449
[507]1450        ## Read content parts from content.
1451        #
1452        context.content_parts = self.get_message_parts(content)
1453        context.content_parts = self.unique_attachment_names(
1454          context.content_parts)
[359]1455
[507]1456        return context
[359]1457
[507]1458    def _add_topic(self, api, context, topic):
1459        context.req.perm.assert_permission('DISCUSSION_APPEND')
[359]1460
[507]1461        ## Filter topic.
1462        #
1463        for discussion_filter in api.discussion_filters:
1464            accept, topic_or_error = discussion_filter.filter_topic(
1465              context, topic)
1466            if accept:
1467                topic = topic_or_error
1468            else:
1469                raise TracError(topic_or_error)
[359]1470
[507]1471        ## Add a new topic.
1472        #
1473        api.add_topic(context, topic)
[359]1474
[507]1475        ## Get inserted topic with new ID.
1476        #
1477        topic = api.get_topic_by_time(context, topic['time'])
[359]1478
[507]1479        ## Attach attachments.
1480        #
1481        self.id = topic['id']
1482        self.attach_attachments(context.content_parts, True)
[359]1483
[507]1484        ## Notify change listeners.
1485        #
1486        for listener in api.topic_change_listeners:
1487            listener.topic_created(context, topic)
[359]1488
[507]1489    def _add_message(self, api, context, message):
1490        context.req.perm.assert_permission('DISCUSSION_APPEND')
[359]1491
[507]1492        ## Filter message.
1493        #
1494        for discussion_filter in api.discussion_filters:
1495            accept, message_or_error = discussion_filter.filter_message(
1496              context, message)
1497            if accept:
1498                message = message_or_error
1499            else:
1500                raise TracError(message_or_error)
[359]1501
[507]1502        ## Add message.
1503        #
1504        api.add_message(context, message)
[359]1505
[507]1506        ## Get inserted message with new ID.
1507        #
1508        message = api.get_message_by_time(context, message['time'])
[359]1509
[507]1510        ## Attach attachments.
1511        #
1512        self.id = message['topic']
1513        self.attach_attachments(context.content_parts, True)
[359]1514
[507]1515        ## Notify change listeners.
1516        #
1517        for listener in api.message_change_listeners:
1518            listener.message_created(context, message)
[359]1519
1520########## MAIN function  ######################################################
1521
[507]1522    def parse(self, fp):
1523        """
1524        """
1525        self.logger.debug('Main function parse')
1526        global m
[96]1527
[507]1528        m = email.message_from_file(fp)
1529       
1530        if not m:
1531            self.logger.debug('This is not a valid email message format')
1532            return
1533           
1534        ## Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1535        #
1536        try:
1537            m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1538        except AttributeError, detail:
1539            pass
[239]1540
[507]1541        if self.parameters.debug:     # save the entire e-mail message text
1542            self.save_email_for_debug(m, True)
[77]1543
[507]1544        self.db = self.env.get_db_cnx()
1545        self.get_sender_info(m)
[152]1546
[507]1547        if not self.email_header_acl('white_list', self.email_addr, True):
1548            self.logger.info('Message rejected : %s not in white list' %(self.email_addr))
1549            return False
[77]1550
[507]1551        if self.email_header_acl('black_list', self.email_addr, False):
1552            self.logger.info('Message rejected : %s in black list' %(self.email_addr))
1553            return False
[221]1554
[507]1555        if not self.email_header_acl('recipient_list', self.to_email_addr, True):
1556            self.logger.info('Message rejected : %s not in recipient list' %(self.to_email_addr))
1557            return False
[226]1558
[507]1559        ## If spam drop the message
1560        #
1561        if self.spam(m) == 'drop':
1562            return False
[194]1563
[507]1564        elif self.spam(m) == 'spam':
1565            spam_msg = True
1566        else:
1567            spam_msg = False
[204]1568
[507]1569        if not m['Subject']:
1570            subject  = 'No Subject'
1571        else:
1572            subject  = self.email_to_unicode(m['Subject'])
[304]1573
[516]1574        self.logger.info('subject: %s' %subject)
[359]1575
[507]1576        ## [hic] #1529: Re: LRZ
1577        #  [hic] #1529?owner=bas,priority=medium: Re: LRZ
1578        #
1579        ticket_regex = r'''
1580            (?P<new_fields>[#][?].*)
1581            |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
1582            '''
1583        ## Check if  FullBlogPlugin is installed
1584        #
1585        blog_enabled = None
1586        blog_regex = ''
1587        if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1588            self.logger.debug('Trac BLOG support enabled')
1589            blog_enabled = True
1590            blog_regex = '''|(?P<blog>blog:(?P<blog_id>\w*))'''
[329]1591
[77]1592
[507]1593        ## Check if DiscussionPlugin is installed
1594        #
1595        discussion_enabled = None
1596        discussion_regex = ''
1597        if self.get_config('components', 'tracdiscussion.api.discussionapi') in ['enabled']:
1598            self.logger.debug('Trac Discussion support enabled')
1599            discussion_enabled = True
1600            discussion_regex = r'''
1601            |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
1602            |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
1603            |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
1604            '''
[77]1605
[359]1606
[507]1607        regex_str = ticket_regex + blog_regex + discussion_regex
1608        SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
[359]1609
[507]1610        ## Find out if this is a ticket, a blog or a discussion
1611        #
1612        result =  SYSTEM_RE.search(subject)
[390]1613
[507]1614        if result:
1615            ## update ticket + fields
1616            #
1617            if result.group('reply'):
1618                self.system = 'ticket'
[260]1619
[507]1620                ## Skip the last ':' character
1621                #
1622                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
1623                    self.new_ticket(m, subject, spam_msg)
[390]1624
[507]1625            ## New ticket + fields
1626            #
1627            elif result.group('new_fields'):
1628                self.system = 'ticket'
1629                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
[262]1630
[507]1631            if blog_enabled:
1632                if result.group('blog'):
1633                    self.system = 'blog'
1634                    self.blog(result.group('blog_id'))
[359]1635
[507]1636            if discussion_enabled:
1637                ## New topic.
1638                #
1639                if result.group('forum'):
1640                    self.system = 'discussion'
1641                    self.id = int(result.group('forum_id'))
1642                    self.discussion_topic(m, subject[result.end('forum'):])
[359]1643
[507]1644                ## Reply to topic.
1645                #
1646                elif result.group('topic'):
1647                    self.system = 'discussion'
1648                    self.id = int(result.group('topic_id'))
1649                    self.discussion_topic_reply(m, subject[result.end('topic'):])
[359]1650
[507]1651                ## Reply to topic message.
1652                #
1653                elif result.group('message'):
1654                    self.system = 'discussion'
1655                    self.id = int(result.group('message_id'))
1656                    self.discussion_message_reply(m, subject[result.end('message'):])
[359]1657
[507]1658        else:
1659            self.system = 'ticket'
1660            (matched_id, subject) = self.ticket_update_by_subject(subject)
1661            if matched_id:
1662                if not self.ticket_update(m, matched_id, spam_msg):
1663                    self.new_ticket(m, subject, spam_msg)
1664            else:
1665                ## No update by subject, so just create a new ticket
1666                #
1667                self.new_ticket(m, subject, spam_msg)
[353]1668
[356]1669
[343]1670########## BODY TEXT functions  ###########################################################
1671
[507]1672    def strip_signature(self, text):
1673        """
1674        Strip signature from message, inspired by Mailman software
1675        """
1676        self.logger.debug('function strip_signature')
[462]1677
[507]1678        body = []
1679        for line in text.splitlines():
1680            if line == '-- ':
1681                break
1682            body.append(line)
[136]1683
[507]1684        return ('\n'.join(body))
[136]1685
[507]1686    def reflow(self, text, delsp = 0):
1687        """
1688        Reflow the message based on the format="flowed" specification (RFC 3676)
1689        """
1690        flowedlines = []
1691        quotelevel = 0
1692        prevflowed = 0
[231]1693
[507]1694        for line in text.splitlines():
1695            from re import match
1696           
1697            ## Figure out the quote level and the content of the current line
1698            #
1699            m = match('(>*)( ?)(.*)', line)
1700            linequotelevel = len(m.group(1))
1701            line = m.group(3)
[231]1702
[507]1703            ## Determine whether this line is flowed
1704            #
1705            if line and line != '-- ' and line[-1] == ' ':
1706                flowed = 1
1707            else:
1708                flowed = 0
[231]1709
[507]1710            if flowed and delsp and line and line[-1] == ' ':
1711                line = line[:-1]
[231]1712
[507]1713            ## If the previous line is flowed, append this line to it
1714            #
1715            if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1716                flowedlines[-1] += line
[477]1717
[507]1718            ## Otherwise, start a new line
1719            #
1720            else:
1721                flowedlines.append('>' * linequotelevel + line)
[231]1722
[507]1723            prevflowed = flowed
1724           
[231]1725
[507]1726        return '\n'.join(flowedlines)
[231]1727
[507]1728    def strip_quotes(self, text):
1729        """
1730        Strip quotes from message by Nicolas Mendoza
1731        """
1732        self.logger.debug('function strip_quotes')
[462]1733
[507]1734        body = []
1735        for line in text.splitlines():
1736            try:
[462]1737
[507]1738                if line.startswith(self.parameters.email_quote):
1739                    continue
[462]1740
[507]1741            except UnicodeDecodeError:
[462]1742
[507]1743                tmp_line = self.email_to_unicode(line)
1744                if tmp_line.startswith(self.parameters.email_quote):
1745                    continue
1746               
1747            body.append(line)
[151]1748
[507]1749        return ('\n'.join(body))
[191]1750
[507]1751    def inline_properties(self, text):
1752        """
1753        Parse text if we use inline keywords to set ticket fields
1754        """
1755        self.logger.debug('function inline_properties')
[309]1756
[507]1757        properties = dict()
1758        body = list()
[309]1759
[507]1760        INLINE_EXP = re.compile('\s*[@]\s*(\w+)\s*:(.*)$')
[309]1761
[507]1762        for line in text.splitlines():
1763            match = INLINE_EXP.match(line)
1764            if match:
1765                keyword, value = match.groups()
[509]1766
1767                if self.parameters.inline_properties_first_wins:
1768                    if keyword in self.properties.keys():
1769                        continue
1770
[507]1771                self.properties[keyword] = value.strip()
[509]1772                self.logger.debug('\tinline properties: %s : %s' %(keyword,value))
[408]1773
[507]1774            else:
1775                body.append(line)
1776               
1777        return '\n'.join(body)
[309]1778
1779
[507]1780    def wrap_text(self, text, replace_whitespace = False):
1781        """
1782        Will break a lines longer then given length into several small
1783        lines of size given length
1784        """
1785        import textwrap
[154]1786
[507]1787        LINESEPARATOR = '\n'
1788        reformat = ''
[151]1789
[507]1790        for s in text.split(LINESEPARATOR):
1791            tmp = textwrap.fill(s, self.parameters.use_textwrap)
1792            if tmp:
1793                reformat = '%s\n%s' %(reformat,tmp)
1794            else:
1795                reformat = '%s\n' %reformat
[153]1796
[507]1797        return reformat
[153]1798
[507]1799        # Python2.4 and higher
1800        #
1801        #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1802        #
[154]1803
[343]1804########## EMAIL attachements functions ###########################################################
1805
[507]1806    def inline_part(self, part):
1807        """
1808        """
1809        self.logger.debug('function inline_part()')
[154]1810
[507]1811        return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
[340]1812
[507]1813    def get_message_parts(self, msg):
1814        """
1815        parses the email message and returns a list of body parts and attachments
1816        body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1817        """
1818        self.logger.debug('function get_message_parts()')
[317]1819
[507]1820        message_parts = list()
1821   
1822        ALTERNATIVE_MULTIPART = False
[278]1823
[507]1824        for part in msg.walk():
1825            content_maintype = part.get_content_maintype()
1826            content_type =  part.get_content_type()
[278]1827
[507]1828            self.logger.debug('\t Message part: Main-Type: %s' % content_maintype)
1829            self.logger.debug('\t Message part: Content-Type: %s' % content_type)
[498]1830
[507]1831            ## Check content type
1832            #
1833            if content_type in self.STRIP_CONTENT_TYPES:
1834                self.logger.debug("\t A %s attachment named '%s' was skipped" %(content_type, part.get_filename()))
1835                continue
[238]1836
[507]1837            ## Catch some mulitpart execptions
1838            #
1839            if content_type == 'multipart/alternative':
1840                ALTERNATIVE_MULTIPART = True
1841                continue
[278]1842
[507]1843            ## Skip multipart containers
1844            #
1845            if content_maintype == 'multipart':
1846                self.logger.debug("\t Skipping multipart container")
1847                continue
1848           
1849            ## Check if this is an inline part. It's inline if there is co Cont-Disp header, or if there is one and it says "inline"
1850            #
1851            inline = self.inline_part(part)
[236]1852
[507]1853            ## Drop HTML message
1854            #
1855            if ALTERNATIVE_MULTIPART and self.parameters.drop_alternative_html_version:
1856                if content_type == 'text/html':
1857                    self.logger.debug('\t Skipping alternative HTML message')
1858                    ALTERNATIVE_MULTIPART = False
1859                    continue
[278]1860
[498]1861
[507]1862            ## Save all non plain text message as attachment
1863            #
1864            if not content_type in ['text/plain']:
[498]1865
[507]1866                if self.parameters.debug:
1867                    s = '\t Filename: %s' % part.get_filename()
1868                    self.print_unicode(s)
[498]1869
[507]1870                ## First try to use email header function to convert filename.
1871                #  If this fails the use the plain filename
1872                #
1873                try:
1874                    filename = self.email_to_unicode(part.get_filename())
1875                except UnicodeEncodeError, detail:
1876                    filename = part.get_filename()
[498]1877
[507]1878                message_parts.append((filename, part))
[498]1879
[507]1880                ## We van only convert html messages
1881                #
1882                if not content_type == 'text/html':
1883                    self.logger.debug('\t Appending content_type = %s' %(content_type))
1884                    continue
[500]1885
[507]1886            if not inline:
1887                    self.logger.debug('\t Skipping %s, not an inline messsage part' %(content_type))
1888                    continue
1889               
1890            ## Try to decode message part. We have a html or plain text messafe
1891            #
1892            body_text = part.get_payload(decode=1)
1893            if not body_text:           
1894                body_text = part.get_payload(decode=0)
[236]1895
[507]1896            ## Try to convert html message
1897            #
1898            if content_type == 'text/html':
[502]1899
[507]1900                body_text = self.html_2_txt(body_text)
1901                if not body_text:
1902                    continue
[498]1903
[507]1904            format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1905            delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
[231]1906
[507]1907            if self.parameters.reflow and not self.parameters.verbatim_format and format == 'flowed':
1908                body_text = self.reflow(body_text, delsp == 'yes')
1909   
1910            if self.parameters.strip_signature:
1911                body_text = self.strip_signature(body_text)
[22]1912
[507]1913            if self.parameters.strip_quotes:
1914                body_text = self.strip_quotes(body_text)
[191]1915
[507]1916            if self.parameters.inline_properties:
1917                body_text = self.inline_properties(body_text)
[309]1918
[507]1919            if self.parameters.use_textwrap:
1920                body_text = self.wrap_text(body_text)
[148]1921
[507]1922            ## Get contents charset (iso-8859-15 if not defined in mail headers)
1923            #
1924            charset = part.get_content_charset()
1925            if not charset:
1926                charset = 'iso-8859-15'
[102]1927
[507]1928            try:
1929                ubody_text = unicode(body_text, charset)
[100]1930
[507]1931            except UnicodeError, detail:
1932                ubody_text = unicode(body_text, 'iso-8859-15')
[89]1933
[507]1934            except LookupError, detail:
1935                ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
[100]1936
[507]1937            if self.parameters.verbatim_format:
1938                message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1939            else:
1940                message_parts.append('%s' %ubody_text)
[22]1941
[507]1942        return message_parts
1943       
1944    def unique_attachment_names(self, message_parts):
1945        """
1946        Make sure we have unique names attachments:
1947          - check if it contains illegal characters
1948          - Rename "None" filenames to "untitled-part"
1949        """
1950        self.logger.debug('function unique_attachment_names()')
1951        renamed_parts = []
1952        attachment_names = set()
[296]1953
[507]1954        for item in message_parts:
1955           
1956            ## If not an attachment, leave it alone
1957            #
1958            if not isinstance(item, tuple):
1959                renamed_parts.append(item)
1960                continue
1961               
1962            (filename, part) = item
[295]1963
[507]1964            ## If filename = None, use a default one
1965            #
1966            if filename in [ 'None']:
1967                filename = 'untitled-part'
1968                self.logger.info('Rename filename "None" to: %s' %filename)
[22]1969
[507]1970                ## Guess the extension from the content type, use non strict mode
1971                #  some additional non-standard but commonly used MIME types
1972                #  are also recognized
1973                #
1974                ext = mimetypes.guess_extension(part.get_content_type(), False)
1975                if not ext:
1976                    ext = '.bin'
[22]1977
[507]1978                filename = '%s%s' % (filename, ext)
[22]1979
[507]1980            ## Discard relative paths for windows/unix in attachment names
1981            #
1982            #filename = filename.replace('\\', '/').replace(':', '/')
1983            filename = filename.replace('\\', '_')
1984            filename = filename.replace('/', '_')
[347]1985
[507]1986            ## remove linefeed char
1987            #
1988            for forbidden_char in ['\r', '\n']:
1989                filename = filename.replace(forbidden_char,'')
[465]1990
[507]1991            ## We try to normalize the filename to utf-8 NFC if we can.
1992            #  Files uploaded from OS X might be in NFD.
1993            #  Check python version and then try it
1994            #
1995            #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1996            #   try:
1997            #       filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1998            #   except TypeError:
1999            #       pass
[100]2000
[507]2001            ## Make the filename unique for this ticket
2002            #
2003            num = 0
2004            unique_filename = filename
2005            dummy_filename, ext = os.path.splitext(filename)
[134]2006
[507]2007            while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
2008                num += 1
2009                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
2010               
2011            if self.parameters.debug:
2012                s = 'Attachment with filename %s will be saved as %s' % (filename, unique_filename)
2013                self.print_unicode(s)
[100]2014
[507]2015            attachment_names.add(unique_filename)
[236]2016
[507]2017            renamed_parts.append((filename, unique_filename, part))
2018   
2019        return renamed_parts
2020           
2021           
2022    def attachment_exists(self, filename):
[250]2023
[507]2024        if self.parameters.debug:
2025            s = 'attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
2026            self.print_unicode(s)
[250]2027
[507]2028        ## We have no valid ticket id
2029        #
2030        if not self.id:
2031            return False
[250]2032
[507]2033        try:
2034            if self.system == 'discussion':
2035                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s'
2036                  % (self.id,), filename)
2037            else:
2038                att = attachment.Attachment(self.env, 'ticket', self.id,
2039                  filename)
2040            return True
2041        except attachment.ResourceNotFound:
2042            return False
[343]2043
2044########## TRAC Ticket Text ###########################################################
[507]2045           
2046    def get_body_text(self, message_parts):
2047        """
2048        """
2049        self.logger.debug('function get_body_text()')
[498]2050
[507]2051        body_text = []
2052       
2053        for part in message_parts:
2054       
2055            ## Plain text part, append it
2056            #
2057            if not isinstance(part, tuple):
2058                body_text.extend(part.strip().splitlines())
2059                body_text.append("")
2060                continue
[490]2061
[507]2062            (original, filename, part) = part
2063            inline = self.inline_part(part)
[491]2064
[507]2065            ## Skip generation of attachment link if html is converted to text
2066            #
2067            if part.get_content_type() == 'text/html' and self.parameters.html2text_cmd and inline:
2068                s = 'Skipping attachment link for html part: %s' %(filename)
2069                self.print_unicode(s)
2070                continue
2071           
2072            if part.get_content_maintype() == 'image' and inline:
2073                if self.system != 'discussion':
2074                    s = 'wiki image link for: %s' %(filename)
2075                    self.print_unicode(s)
2076                    body_text.append('[[Image(%s)]]' % filename)
2077                body_text.append("")
2078            else:
2079                if self.system != 'discussion':
2080                    s = 'wiki attachment link for: %s' %(filename)
2081                    self.print_unicode(s)
2082                    body_text.append('[attachment:"%s"]' % filename)
2083                body_text.append("")
[490]2084
[507]2085        ## Convert list body_texts to string
2086        #
2087        body_text = '\r\n'.join(body_text)
2088        return body_text
[236]2089
[507]2090    def html_mailto_link(self, subject):
2091        """
2092        This function returns a HTML mailto tag with the ticket id and author email address
2093        """
2094        self.logger.debug("function html_mailto_link")
2095        if not self.author:
2096            author = self.email_addr
2097        else:   
2098            author = self.author
[343]2099
[507]2100        if not self.parameters.mailto_cc:
2101            self.parameters.mailto_cc = ''
[434]2102
[507]2103        ## use urllib to escape the chars
2104        #
[517]2105        s = '%s?Subject=%s&cc=%s' %(
[507]2106               urllib.quote(self.email_addr),
2107               urllib.quote('Re: #%s: %s' %(self.id, subject)),
2108               urllib.quote(self.parameters.mailto_cc)
2109               )
[343]2110
[507]2111        if self.VERSION in [ 0.10 ]:
2112            s = '\r\n{{{\r\n#!html\r\n<a\r\n href="mailto:%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author)
2113        else:
2114            s = '[mailto:"%s" Reply to: %s]' %(s, author)
[506]2115
[507]2116        self.logger.debug("\tmailto link %s" %s)
2117        return s
[343]2118
2119########## TRAC notify section ###########################################################
2120
[507]2121    def notify(self, tkt, new=True, modtime=0):
2122        """
2123        A wrapper for the TRAC notify function. So we can use templates
2124        """
2125        self.logger.debug('function notify()')
[392]2126
[509]2127        if self.parameters.notify_reporter:
2128            self.logger.debug('\t Notify reporter set')
[508]2129            global sender_email
2130            sender_email = self.email_addr
2131 
2132            self.logger.debug('\t Using Email2TracNotification function AlwaysNotifyReporter')
2133            import trac.notification as Email2TracNotification
2134            Email2TracNotification.Notify.notify = AlwaysNotifyReporter
2135
2136        if self.parameters.dry_run  :
[507]2137                print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
2138                return
2139        try:
[392]2140
[507]2141            #from trac.ticket.web_ui import TicketModule
2142            #from trac.ticket.notification import TicketNotificationSystem
2143            #ticket_sys = TicketNotificationSystem(self.env)
2144            #a = TicketModule(self.env)
2145            #print a.__dict__
2146            #tn_sys = TicketNotificationSystem(self.env)
2147            #print tn_sys
2148            #print tn_sys.__dict__
2149            #sys.exit(0)
[392]2150
[507]2151            ## create false {abs_}href properties, to trick Notify()
2152            #
2153            if not (self.VERSION in [0.11, 0.12]):
2154                self.env.abs_href = Href(self.get_config('project', 'url'))
2155                self.env.href = Href(self.get_config('project', 'url'))
[22]2156
[507]2157            tn = TicketNotifyEmail(self.env)
[213]2158
[507]2159            if self.parameters.alternate_notify_template:
[222]2160
[507]2161                if self.VERSION >= 0.11:
[222]2162
[507]2163                    from trac.web.chrome import Chrome
[222]2164
[507]2165                    if  self.parameters.alternate_notify_template_update and not new:
2166                        tn.template_name = self.parameters.alternate_notify_template_update
2167                    else:
2168                        tn.template_name = self.parameters.alternate_notify_template
[222]2169
[507]2170                    tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
2171                       
2172                else:
[222]2173
[507]2174                    tn.template_name = self.parameters.alternate_notify_template
[42]2175
[507]2176            tn.notify(tkt, new, modtime)
[41]2177
[507]2178        except Exception, e:
2179            self.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e))
[41]2180
[508]2181########## END Class Definition  ########################################################
[22]2182
[508]2183########## Global Notificaition Function ################################################
2184def AlwaysNotifyReporter(self, resid):
2185    """
2186    Copy of def notify() to manipulate recipents to always include reporter for the
2187    notification.
2188    """
2189    (torcpts, ccrcpts) = self.get_recipients(resid)
[74]2190
[508]2191    if not tktparser.email_header_acl('notify_reporter_black_list', sender_email, False):
2192        ## additionally append sender (regardeless of settings in trac.ini)
2193        #
2194        if not sender_email in torcpts:
2195            torcpts.append(sender_email)
2196
2197    self.begin_send()
2198    self.send(torcpts, ccrcpts)
2199    self.finish_send()
2200
[343]2201########## Parse Config File  ###########################################################
[22]2202
2203def ReadConfig(file, name):
[507]2204    """
2205    Parse the config file
2206    """
2207    if not os.path.isfile(file):
2208        print 'File %s does not exist' %file
2209        sys.exit(1)
[22]2210
[507]2211    config = trac_config.Configuration(file)
2212   
2213    parentdir = config.get('DEFAULT', 'parentdir')
2214    sections = config.sections()
[22]2215
[507]2216    ## use some trac internals to get the defaults
2217    #
2218    tmp = config.parser.defaults()
2219    project =  SaraDict()
[485]2220
[507]2221    for option, value in tmp.items():
2222        try:
2223            project[option] = int(value)
2224        except ValueError:
2225            project[option] = value
[485]2226
[507]2227    if name:
2228        if name in sections:
2229            project =  SaraDict()
2230            for option, value in  config.options(name):
2231                try:
2232                    project[option] = int(value)
2233                except ValueError:
2234                    project[option] = value
[485]2235
[507]2236        elif not parentdir:
2237            print "Not a valid project name: %s, valid names are: %s" %(name, sections)
2238            print "or set parentdir in the [DEFAULT] section"
2239            sys.exit(1)
[22]2240
[507]2241    ## If parentdir then set project dir to parentdir + name
2242    #
2243    if not project.has_key('project'):
2244        if not parentdir:
2245            print "You must set project or parentdir in your configuration file"
2246            sys.exit(1)
2247        elif not name:
2248            print "You must configure a  project section in your configuration file"
2249        else:
2250            project['project'] = os.path.join(parentdir, name)
[22]2251
[507]2252    return project
[22]2253
[410]2254########## Setup Logging ###############################################################
[87]2255
[419]2256def setup_log(parameters, project_name, interactive=None):
[507]2257    """
[516]2258    Setup logging
[410]2259
[507]2260    Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
[420]2261    would be interpreted by the ConfigParser itself.
[507]2262    """
2263    logger = logging.getLogger('email2trac %s' %project_name)
[410]2264
[507]2265    if interactive:
2266        parameters.log_type = 'stderr'
[414]2267
[507]2268    if not parameters.log_type:
2269        if sys.platform in ['win32', 'cygwin']:
[521]2270            parameters.log_type = 'eventlog'
[507]2271        else:
2272            parameters.log_type = 'syslog'
[410]2273
[507]2274    if parameters.log_type == 'file':
[415]2275
[507]2276        if not parameters.log_file:
2277            parameters.log_file = 'email2trac.log'
[415]2278
[507]2279        if not os.path.isabs(parameters.log_file):
2280            parameters.log_file = os.path.join(tempfile.gettempdir(), parameters.log_file)
[410]2281
[507]2282        log_handler = logging.FileHandler(parameters.log_file)
[415]2283
[507]2284    elif parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
2285        ## Requires win32 extensions
2286        #
2287        log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application')
[410]2288
[507]2289    elif parameters.log_type in ('syslog', 'unix'):
2290        log_handler = logging.handlers.SysLogHandler('/dev/log')
[410]2291
[507]2292    elif parameters.log_type in ('stderr'):
2293        log_handler = logging.StreamHandler(sys.stderr)
[410]2294
[507]2295    else:
2296        log_handler = logging.handlers.BufferingHandler(0)
[410]2297
[507]2298    if parameters.log_format:
2299        parameters.log_format = parameters.log_format.replace('$(', '%(')
2300    else:
2301        parameters.log_format = '%(name)s: %(message)s'
[516]2302        if parameters.log_type in ('file', 'stderr'):
2303            parameters.log_format = '%(asctime)s ' + parameters.log_format
[410]2304
[507]2305    log_formatter = logging.Formatter(parameters.log_format)
2306    log_handler.setFormatter(log_formatter)
2307    logger.addHandler(log_handler)
[410]2308
[507]2309    if (parameters.log_level in ['DEBUG', 'ALL']) or (parameters.debug > 0):
2310        logger.setLevel(logging.DEBUG)
2311        parameters.debug = 1
[410]2312
[507]2313    elif parameters.log_level in ['INFO'] or parameters.verbose:
2314        logger.setLevel(logging.INFO)
[410]2315
[507]2316    elif parameters.log_level in ['WARNING']:
2317        logger.setLevel(logging.WARNING)
[410]2318
[507]2319    elif parameters.log_level in ['ERROR']:
2320        logger.setLevel(logging.ERROR)
[410]2321
[507]2322    elif parameters.log_level in ['CRITICAL']:
2323        logger.setLevel(logging.CRITICAL)
[410]2324
[507]2325    else:
2326        logger.setLevel(logging.INFO)
[410]2327
[507]2328    return logger
[410]2329
2330
[22]2331if __name__ == '__main__':
[507]2332    ## Default config file
2333    #
2334    configfile = '@email2trac_conf@'
2335    project = ''
2336    component = ''
2337    ticket_prefix = 'default'
2338    dry_run = None
2339    verbose = None
2340    debug_interactive = None
[202]2341
[507]2342    SHORT_OPT = 'cdhf:np:t:v'
2343    LONG_OPT  =  ['component=', 'debug', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
[201]2344
[507]2345    try:
2346        opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
2347    except getopt.error,detail:
2348        print __doc__
2349        print detail
2350        sys.exit(1)
2351   
2352    project_name = None
2353    for opt,value in opts:
2354        if opt in [ '-h', '--help']:
2355            print __doc__
2356            sys.exit(0)
2357        elif opt in ['-c', '--component']:
2358            component = value
2359        elif opt in ['-d', '--debug']:
2360            debug_interactive = 1
2361        elif opt in ['-f', '--file']:
2362            configfile = value
2363        elif opt in ['-n', '--dry-run']:
2364            dry_run = True
2365        elif opt in ['-p', '--project']:
2366            project_name = value
2367        elif opt in ['-t', '--ticket_prefix']:
2368            ticket_prefix = value
2369        elif opt in ['-v', '--verbose']:
2370            verbose = True
2371   
2372    settings = ReadConfig(configfile, project_name)
[410]2373
[507]2374    ## The default prefix for ticket values in email2trac.conf
2375    #
2376    settings.ticket_prefix = ticket_prefix
2377    settings.dry_run = dry_run
2378    settings.verbose = verbose
[410]2379
[507]2380    if not settings.debug and debug_interactive:
2381        settings.debug = debug_interactive
[412]2382
[507]2383    if not settings.project:
2384        print __doc__
2385        print 'No Trac project is defined in the email2trac config file.'
2386        sys.exit(1)
[419]2387
[507]2388    logger = setup_log(settings, os.path.basename(settings.project), debug_interactive)
2389   
2390    if component:
2391        settings['component'] = component
[202]2392
[507]2393    ## Determine major trac version used to be in email2trac.conf
2394    # Quick hack for 0.12
2395    #
2396    version = '0.%s' %(trac_version.split('.')[1])
2397    if version.startswith('0.12'):
2398        version = '0.12'
[373]2399
[507]2400    logger.debug("Found trac version: %s" %(version))
2401   
2402    try:
2403        if version == '0.10':
2404            from trac import attachment
2405            from trac.env import Environment
2406            from trac.ticket import Ticket
2407            from trac.web.href import Href
2408            from trac import util
2409            from trac.ticket.web_ui import TicketModule
[486]2410
[507]2411            #
2412            # return  util.text.to_unicode(str)
2413            #
2414            # see http://projects.edgewall.com/trac/changeset/2799
2415            from trac.ticket.notification import TicketNotifyEmail
2416            from trac import config as trac_config
2417            from trac.core import TracError
[359]2418
[507]2419        elif version == '0.11':
2420            from trac import attachment
2421            from trac.env import Environment
2422            from trac.ticket import Ticket
2423            from trac.web.href import Href
2424            from trac import config as trac_config
2425            from trac import util
2426            from trac.core import TracError
2427            from trac.perm import PermissionSystem
2428            from trac.ticket.web_ui import TicketModule
[260]2429
[507]2430            #
2431            # return  util.text.to_unicode(str)
2432            #
2433            # see http://projects.edgewall.com/trac/changeset/2799
2434            from trac.ticket.notification import TicketNotifyEmail
[260]2435
[507]2436        elif version == '0.12':
2437            from trac import attachment
2438            from trac.env import Environment
2439            from trac.ticket import Ticket
2440            from trac.web.href import Href
2441            from trac import config as trac_config
2442            from trac import util
2443            from trac.core import TracError
2444            from trac.perm import PermissionSystem
2445            from trac.ticket.web_ui import TicketModule
[368]2446
[507]2447            #
2448            # return  util.text.to_unicode(str)
2449            #
2450            # see http://projects.edgewall.com/trac/changeset/2799
2451            from trac.ticket.notification import TicketNotifyEmail
[368]2452
2453
[507]2454        else:
2455            logger.error('TRAC version %s is not supported' %version)
[519]2456            sys.exit(0)
[182]2457
[507]2458        ## Must be set before environment is created
2459        #
2460        if settings.has_key('python_egg_cache'):
2461            python_egg_cache = str(settings['python_egg_cache'])
2462            os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
[291]2463
[507]2464        if settings.debug > 0:
2465            logger.debug('Loading environment %s', settings.project)
[359]2466
[507]2467        try:
2468            env = Environment(settings['project'], create=0)
2469        except IOError, detail:
[519]2470            logger.error("Trac project does not exists: %s" %settings['project'])
2471            sys.exit(0)
[333]2472
[507]2473        tktparser = TicketEmailParser(env, settings, logger, float(version))
2474        tktparser.parse(sys.stdin)
[22]2475
[507]2476    ## Catch all errors and use the logging module
2477    #
2478    except Exception, error:
[187]2479
[507]2480        etype, evalue, etb = sys.exc_info()
2481        for e in traceback.format_exception(etype, evalue, etb):
2482            logger.critical(e)
[187]2483
[507]2484        if m:
2485            tktparser.save_email_for_debug(m, True)
[97]2486
[507]2487        sys.exit(1)
[22]2488# EOB
Note: See TracBrowser for help on using the repository browser.