source: trunk/email2trac.py.in @ 592

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

some minor reformating, tabs to spaces

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