source: trunk/email2trac.py.in @ 580

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

added Astarp firewall support:

X-Spam-Flag: YES
X-Spam-Result: Spam

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