source: trunk/email2trac.py.in @ 572

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

adjusted email_header_acl we now loop over all to address to allow regexs like (*.sara.nl or basv@…), see #272

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