source: trunk/email2trac.py.in @ 604

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

applied and modified patch to skip stripping of signatures and quotes, closes #192

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