source: trunk/email2trac.py.in @ 509

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

added a new parameter: inline_properties_first_wins, closes #216

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