source: trunk/email2trac.py.in @ 587

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

email2trac.py:

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