source: trunk/email2trac.py.in @ 524

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

Fixed and error in ticket_update_fields. Always check if it is a valid ticket field for trac

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