source: trunk/email2trac.py.in @ 578

Last change on this file since 578 was 578, checked in by bas, 12 years ago

commited patch by thomas moschny for blog support, see #235

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