source: trunk/email2trac.py.in @ 595

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

changed license header, see #292, thanks

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