source: trunk/email2trac.py.in @ 576

Last change on this file since 576 was 576, checked in by bas, 11 years ago

fixed an error in parsing from addres if email name contains a charset

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