source: trunk/email2trac.py.in @ 582

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

urllib.quote can not handele unicode strings

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 81.6 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 582 2011-12-20 10:38:10Z 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       
1242            #mailto = self.html_mailto_link( m['Subject'])
1243            mailto = self.html_mailto_link(subject)
1244
1245            tkt['description'] = u'%s\r\n%s%s\r\n' \
1246                %(head, mailto, body_text)
1247   
1248        ## Save the attachments to the ticket   
1249        #
1250        error_with_attachments =  self.attach_attachments(message_parts)
1251
1252        if error_with_attachments:
1253            changed = True
1254            comment = '%s\n%s\n' %(comment, error_with_attachments)
1255
1256        if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
1257            if self.mail_workflow(tkt):
1258                changed = True
1259
1260        if changed:
1261            if self.parameters.dry_run:
1262                s = 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
1263                self.logger.info(s)
1264
1265            else:
1266                tkt.save_changes(tkt['reporter'], comment)
1267
1268        if not spam:
1269            self.notify(tkt, True)
1270
1271
1272    def attach_attachments(self, message_parts, update=False):
1273        '''
1274        save any attachments as files in the ticket's directory
1275        '''
1276        self.logger.debug('function attach_attachments()')
1277
1278        if self.parameters.dry_run:
1279            self.logger.debug("DRY_RUN: no attachments attached to tickets")
1280            return ''
1281
1282        count = 0
1283
1284        ## Get Maxium attachment size
1285        #
1286        max_size = int(self.get_config('attachment', 'max_size'))
1287        status   = None
1288       
1289        for item in message_parts:
1290            ## Skip body parts
1291            #
1292            if not isinstance(item, tuple):
1293                continue
1294               
1295            (original, filename, part) = item
1296
1297            ## We have to determine the size so we use this temporary solution.
1298            #
1299            path, fd =  util.create_unique_file(os.path.join(self.parameters.tmpdir, 'email2trac_tmp.att'))
1300            text = part.get_payload(decode=1)
1301            if not text:
1302                text = '(None)'
1303            fd.write(text)
1304            fd.close()
1305
1306            ## get the file_size
1307            #
1308            stats = os.lstat(path)
1309            file_size = stats[ST_SIZE]
1310
1311            ## Check if the attachment size is allowed
1312            #
1313            if (max_size != -1) and (file_size > max_size):
1314                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1315                    %(status, original, file_size, max_size)
1316
1317                os.unlink(path)
1318                continue
1319            else:
1320                count = count + 1
1321                   
1322            ## Insert the attachment
1323            #
1324            fd = open(path, 'rb')
1325            if self.system == 'discussion':
1326                att = attachment.Attachment(self.env, 'discussion', 'topic/%s' % (self.id,))
1327            else:
1328                s = 'Attach %s to ticket %d' %(filename, self.id)
1329                self.print_unicode(s)
1330                att = attachment.Attachment(self.env, 'ticket', self.id)
1331 
1332            ## This will break the ticket_update system, the body_text is vaporized
1333            #  ;-(
1334            #
1335            if not update:
1336                att.author = self.author
1337                att.description = self.email_to_unicode('Added by email2trac')
1338
1339            try:
1340
1341                self.logger.debug('Insert atachment')
1342                att.insert(filename, fd, file_size)
1343
1344            except OSError, detail:
1345
1346                self.logger.info('%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail))
1347                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
1348
1349            ## Remove the created temporary filename
1350            #
1351            fd.close()
1352            os.unlink(path)
1353
1354        ## return error
1355        #
1356        return status
1357
1358########## Fullblog functions  #################################################
1359
1360    def blog(self, msg, subject, id):
1361        """
1362        The blog create/update function
1363        """
1364        ## import the modules
1365        #
1366        from tracfullblog.core import FullBlogCore
1367        from tracfullblog.model import BlogPost, BlogComment
1368
1369        ## instantiate blog core
1370        #
1371        blog = FullBlogCore(self.env)
1372        req = Mock(authname='anonymous', perm=MockPerm(), args={})
1373
1374        if id:
1375
1376            ## update blog
1377            #
1378            comment = BlogComment(self.env, id)
1379            comment.author = self.author
1380
1381            message_parts = self.get_message_parts(msg)
1382            comment.comment = self.get_body_text(message_parts)
1383
1384            if self.parameters.dry_run:
1385                self.logger.info('DRY-RUN: not adding comment for blog entry "%s"' % id)
1386                return
1387            blog.create_comment(req, comment)
1388
1389        else:
1390            ## create blog
1391            #
1392            import time
1393            post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
1394
1395            #post = BlogPost(self.env, blog._get_default_postname(self.env))
1396           
1397            post.author = self.author
1398            post.title = subject.strip()
1399
1400            message_parts = self.get_message_parts(msg)
1401            post.body = self.get_body_text(message_parts)
1402           
1403            if self.parameters.dry_run:
1404                self.logger.info('DRY-RUN: not creating blog entry "%s"' % post.title)
1405                return
1406            blog.create_post(req, post, self.author, u'Created by email2trac', False)
1407
1408
1409########## Discussion functions  ##############################################
1410
1411    def discussion_topic(self, content, subject):
1412
1413        ## Import modules.
1414        #
1415        from tracdiscussion.api import DiscussionApi
1416        from trac.util.datefmt import to_timestamp, utc
1417
1418        self.logger.debug('Creating a new topic in forum:', self.id)
1419
1420        ## Get dissussion API component.
1421        #
1422        api = self.env[DiscussionApi]
1423        context = self._create_context(content, subject)
1424
1425        ## Get forum for new topic.
1426        #
1427        forum = api.get_forum(context, self.id)
1428
1429        if not forum:
1430            self.logger.error("ERROR: Replied forum doesn't exist")
1431
1432        ## Prepare topic.
1433        #
1434        topic = {'forum' : forum['id'],
1435                 'subject' : context.subject,
1436                 'time': to_timestamp(datetime.now(utc)),
1437                 'author' : self.author,
1438                 'subscribers' : [self.email_addr],
1439                 'body' : self.get_body_text(context.content_parts)}
1440
1441        ## Add topic to DB and commit it.
1442        #
1443        self._add_topic(api, context, topic)
1444        self.db.commit()
1445
1446    def discussion_topic_reply(self, content, subject):
1447
1448        ## Import modules.
1449        #
1450        from tracdiscussion.api import DiscussionApi
1451        from trac.util.datefmt import to_timestamp, utc
1452
1453        self.logger.debug('Replying to discussion topic', self.id)
1454
1455        ## Get dissussion API component.
1456        #
1457        api = self.env[DiscussionApi]
1458        context = self._create_context(content, subject)
1459
1460        ## Get replied topic.
1461        #
1462        topic = api.get_topic(context, self.id)
1463
1464        if not topic:
1465            self.logger.error("ERROR: Replied topic doesn't exist")
1466
1467        ## Prepare message.
1468        #
1469        message = {'forum' : topic['forum'],
1470                   'topic' : topic['id'],
1471                   'replyto' : -1,
1472                   'time' : to_timestamp(datetime.now(utc)),
1473                   'author' : self.author,
1474                   'body' : self.get_body_text(context.content_parts)}
1475
1476        ## Add message to DB and commit it.
1477        #
1478        self._add_message(api, context, message)
1479        self.db.commit()
1480
1481    def discussion_message_reply(self, content, subject):
1482
1483        ## Import modules.
1484        #
1485        from tracdiscussion.api import DiscussionApi
1486        from trac.util.datefmt import to_timestamp, utc
1487
1488        self.logger.debug('Replying to discussion message', self.id)
1489
1490        ## Get dissussion API component.
1491        #
1492        api = self.env[DiscussionApi]
1493        context = self._create_context(content, subject)
1494
1495        ## Get replied message.
1496        #
1497        message = api.get_message(context, self.id)
1498
1499        if not message:
1500            self.logger.error("ERROR: Replied message doesn't exist")
1501
1502        ## Prepare message.
1503        #
1504        message = {'forum' : message['forum'],
1505                   'topic' : message['topic'],
1506                   'replyto' : message['id'],
1507                   'time' : to_timestamp(datetime.now(utc)),
1508                   'author' : self.author,
1509                   'body' : self.get_body_text(context.content_parts)}
1510
1511        ## Add message to DB and commit it.
1512        #
1513        self._add_message(api, context, message)
1514        self.db.commit()
1515
1516    def _create_context(self, content, subject):
1517
1518        ## Import modules.
1519        #
1520        from trac.mimeview import Context
1521        from trac.web.api import Request
1522        from trac.perm import PermissionCache
1523
1524        ## TODO: Read server base URL from config.
1525        #  Create request object to mockup context creation.
1526        #
1527        environ = {'SERVER_PORT' : 80,
1528                   'SERVER_NAME' : 'test',
1529                   'REQUEST_METHOD' : 'POST',
1530                   'wsgi.url_scheme' : 'http',
1531                   'wsgi.input' : sys.stdin}
1532        chrome =  {'links': {},
1533                   'scripts': [],
1534                   'ctxtnav': [],
1535                   'warnings': [],
1536                   'notices': []}
1537
1538        if self.env.base_url_for_redirect:
1539            environ['trac.base_url'] = self.env.base_url
1540
1541        req = Request(environ, None)
1542        req.chrome = chrome
1543        req.tz = 'missing'
1544        req.authname = self.author
1545        req.perm = PermissionCache(self.env, self.author)
1546        req.locale = None
1547
1548        ## Create and return context.
1549        #
1550        context = Context.from_request(req)
1551        context.realm = 'discussion-email2trac'
1552        context.cursor = self.db.cursor()
1553        context.content = content
1554        context.subject = subject
1555
1556        ## Read content parts from content.
1557        #
1558        context.content_parts = self.get_message_parts(content)
1559        context.content_parts = self.unique_attachment_names(
1560          context.content_parts)
1561
1562        return context
1563
1564    def _add_topic(self, api, context, topic):
1565        context.req.perm.assert_permission('DISCUSSION_APPEND')
1566
1567        ## Filter topic.
1568        #
1569        for discussion_filter in api.discussion_filters:
1570            accept, topic_or_error = discussion_filter.filter_topic(
1571              context, topic)
1572            if accept:
1573                topic = topic_or_error
1574            else:
1575                raise TracError(topic_or_error)
1576
1577        ## Add a new topic.
1578        #
1579        api.add_topic(context, topic)
1580
1581        ## Get inserted topic with new ID.
1582        #
1583        topic = api.get_topic_by_time(context, topic['time'])
1584
1585        ## Attach attachments.
1586        #
1587        self.id = topic['id']
1588        self.attach_attachments(context.content_parts, True)
1589
1590        ## Notify change listeners.
1591        #
1592        for listener in api.topic_change_listeners:
1593            listener.topic_created(context, topic)
1594
1595    def _add_message(self, api, context, message):
1596        context.req.perm.assert_permission('DISCUSSION_APPEND')
1597
1598        ## Filter message.
1599        #
1600        for discussion_filter in api.discussion_filters:
1601            accept, message_or_error = discussion_filter.filter_message(
1602              context, message)
1603            if accept:
1604                message = message_or_error
1605            else:
1606                raise TracError(message_or_error)
1607
1608        ## Add message.
1609        #
1610        api.add_message(context, message)
1611
1612        ## Get inserted message with new ID.
1613        #
1614        message = api.get_message_by_time(context, message['time'])
1615
1616        ## Attach attachments.
1617        #
1618
1619        self.attach_attachments(context.content_parts, True)
1620
1621        ## Notify change listeners.
1622        #
1623        for listener in api.message_change_listeners:
1624            listener.message_created(context, message)
1625
1626########## MAIN function  ######################################################
1627
1628    def parse(self, fp):
1629        """
1630        """
1631        self.logger.debug('Main function parse')
1632        global m
1633
1634        m = email.message_from_file(fp)
1635       
1636        if not m:
1637            self.logger.debug('This is not a valid email message format')
1638            return
1639           
1640        ## Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1641        #
1642        try:
1643            m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1644        except AttributeError, detail:
1645            pass
1646
1647        if self.parameters.debug:     # save the entire e-mail message text
1648            self.save_email_for_debug(m, self.parameters.project_name, True)
1649
1650        self.db = self.env.get_db_cnx()
1651        self.get_sender_info(m)
1652
1653        if not self.email_header_acl('white_list', self.email_addr, True):
1654            self.logger.info('Message rejected : %s not in white list' %(self.email_addr))
1655            return False
1656
1657        if self.email_header_acl('black_list', self.email_addr, False):
1658            self.logger.info('Message rejected : %s in black list' %(self.email_addr))
1659            return False
1660
1661        if not self.email_header_acl('recipient_list', self.email_to_addrs, True):
1662            self.logger.info('Message rejected : %s not in recipient list' %(self.email_to_addrs))
1663            return False
1664
1665        ## If spam drop the message
1666        #
1667        if self.spam(m) == 'drop':
1668            return False
1669
1670        elif self.spam(m) == 'spam':
1671            spam_msg = True
1672        else:
1673            spam_msg = False
1674
1675        if not m['Subject']:
1676            subject  = 'No Subject'
1677        else:
1678            subject  = self.email_to_unicode(m['Subject'])
1679
1680        ## Bug in python logging module <2.6
1681        #
1682        self.logger.info('subject: %s' %repr(subject))
1683
1684        ## [hic] #1529: Re: LRZ
1685        #  [hic] #1529?owner=bas,priority=medium: Re: LRZ
1686        #
1687        ticket_regex = r'''
1688            (?P<new_fields>[#][?].*)
1689            |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
1690            '''
1691        ## Check if  FullBlogPlugin is installed
1692        #
1693        blog_enabled = None
1694        blog_regex = ''
1695        if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1696            self.logger.debug('Trac BLOG support enabled')
1697            blog_enabled = True
1698            blog_regex = '''|(?P<blog>blog:(?P<blog_id>\w*))'''
1699
1700
1701        ## Check if DiscussionPlugin is installed
1702        #
1703        discussion_enabled = None
1704        discussion_regex = ''
1705        if self.get_config('components', 'tracdiscussion.api.discussionapi') in ['enabled']:
1706            self.logger.debug('Trac Discussion support enabled')
1707            discussion_enabled = True
1708            discussion_regex = r'''
1709            |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
1710            |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
1711            |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
1712            '''
1713
1714
1715        regex_str = ticket_regex + blog_regex + discussion_regex
1716        SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
1717
1718        ## Find out if this is a ticket, a blog or a discussion
1719        #
1720        result =  SYSTEM_RE.search(subject)
1721
1722        if result:
1723            ## update ticket + fields
1724            #
1725            if result.group('reply'):
1726                self.system = 'ticket'
1727
1728                ## Skip the last ':' character
1729                #
1730                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
1731                    self.new_ticket(m, subject, spam_msg)
1732
1733            ## New ticket + fields
1734            #
1735            elif result.group('new_fields'):
1736                self.system = 'ticket'
1737                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1738
1739            if blog_enabled:
1740                if result.group('blog'):
1741                    self.system = 'blog'
1742                    self.blog(m, subject[result.end('blog'):], result.group('blog_id'))
1743
1744            if discussion_enabled:
1745                ## New topic.
1746                #
1747                if result.group('forum'):
1748                    self.system = 'discussion'
1749                    self.id = int(result.group('forum_id'))
1750                    self.discussion_topic(m, subject[result.end('forum'):])
1751
1752                ## Reply to topic.
1753                #
1754                elif result.group('topic'):
1755                    self.system = 'discussion'
1756                    self.id = int(result.group('topic_id'))
1757                    self.discussion_topic_reply(m, subject[result.end('topic'):])
1758
1759                ## Reply to topic message.
1760                #
1761                elif result.group('message'):
1762                    self.system = 'discussion'
1763                    self.id = int(result.group('message_id'))
1764                    self.discussion_message_reply(m, subject[result.end('message'):])
1765
1766        else:
1767
1768            self.system = 'ticket'
1769            (matched_id, subject) = self.ticket_update_by_subject(subject)
1770
1771            if matched_id:
1772
1773                if not self.ticket_update(m, matched_id, spam_msg):
1774                    self.new_ticket(m, subject, spam_msg)
1775
1776            else:
1777                ## No update by subject, so just create a new ticket
1778                #
1779                self.new_ticket(m, subject, spam_msg)
1780
1781
1782########## BODY TEXT functions  ###########################################################
1783
1784    def strip_signature(self, text):
1785        """
1786        Strip signature from message, inspired by Mailman software
1787        """
1788        self.logger.debug('function strip_signature')
1789
1790        body = []
1791        for line in text.splitlines():
1792            if line == '-- ':
1793                break
1794            body.append(line)
1795
1796        return ('\n'.join(body))
1797
1798    def reflow(self, text, delsp = 0):
1799        """
1800        Reflow the message based on the format="flowed" specification (RFC 3676)
1801        """
1802        flowedlines = []
1803        quotelevel = 0
1804        prevflowed = 0
1805
1806        for line in text.splitlines():
1807            from re import match
1808           
1809            ## Figure out the quote level and the content of the current line
1810            #
1811            m = match('(>*)( ?)(.*)', line)
1812            linequotelevel = len(m.group(1))
1813            line = m.group(3)
1814
1815            ## Determine whether this line is flowed
1816            #
1817            if line and line != '-- ' and line[-1] == ' ':
1818                flowed = 1
1819            else:
1820                flowed = 0
1821
1822            if flowed and delsp and line and line[-1] == ' ':
1823                line = line[:-1]
1824
1825            ## If the previous line is flowed, append this line to it
1826            #
1827            if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1828                flowedlines[-1] += line
1829
1830            ## Otherwise, start a new line
1831            #
1832            else:
1833                flowedlines.append('>' * linequotelevel + line)
1834
1835            prevflowed = flowed
1836           
1837
1838        return '\n'.join(flowedlines)
1839
1840    def strip_quotes(self, text):
1841        """
1842        Strip quotes from message by Nicolas Mendoza
1843        """
1844        self.logger.debug('function strip_quotes')
1845
1846        body = []
1847        for line in text.splitlines():
1848            try:
1849
1850                if line.startswith(self.parameters.email_quote):
1851                    continue
1852
1853            except UnicodeDecodeError:
1854
1855                tmp_line = self.email_to_unicode(line)
1856                if tmp_line.startswith(self.parameters.email_quote):
1857                    continue
1858               
1859            body.append(line)
1860
1861        return ('\n'.join(body))
1862
1863    def inline_properties(self, text):
1864        """
1865        Parse text if we use inline keywords to set ticket fields
1866        """
1867        self.logger.debug('function inline_properties')
1868
1869        properties = dict()
1870        body = list()
1871
1872        INLINE_EXP = re.compile('\s*[@]\s*(\w+)\s*:(.*)$')
1873
1874        for line in text.splitlines():
1875            match = INLINE_EXP.match(line)
1876            if match:
1877                keyword, value = match.groups()
1878
1879                if self.parameters.inline_properties_first_wins:
1880                    if keyword in self.properties.keys():
1881                        continue
1882
1883                self.properties[keyword] = value.strip()
1884                self.logger.debug('\tinline properties: %s : %s' %(keyword,value))
1885
1886            else:
1887                body.append(line)
1888               
1889        return '\n'.join(body)
1890
1891
1892    def wrap_text(self, text, replace_whitespace = False):
1893        """
1894        Will break a lines longer then given length into several small
1895        lines of size given length
1896        """
1897        import textwrap
1898
1899        LINESEPARATOR = '\n'
1900        reformat = ''
1901
1902        for s in text.split(LINESEPARATOR):
1903            tmp = textwrap.fill(s, self.parameters.use_textwrap)
1904            if tmp:
1905                reformat = '%s\n%s' %(reformat,tmp)
1906            else:
1907                reformat = '%s\n' %reformat
1908
1909        return reformat
1910
1911        # Python2.4 and higher
1912        #
1913        #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1914        #
1915
1916########## EMAIL attachements functions ###########################################################
1917
1918    def inline_part(self, part):
1919        """
1920        """
1921        self.logger.debug('function inline_part()')
1922
1923        return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1924
1925    def get_message_parts(self, msg):
1926        """
1927        parses the email message and returns a list of body parts and attachments
1928        body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1929        """
1930        self.logger.debug('function get_message_parts()')
1931
1932        message_parts = list()
1933   
1934        ALTERNATIVE_MULTIPART = False
1935
1936        for part in msg.walk():
1937            content_maintype = part.get_content_maintype()
1938            content_type =  part.get_content_type()
1939
1940            self.logger.debug('\t Message part: Main-Type: %s' % content_maintype)
1941            self.logger.debug('\t Message part: Content-Type: %s' % content_type)
1942
1943            ## Check content type
1944            #
1945            if content_type in self.STRIP_CONTENT_TYPES:
1946                self.logger.debug("\t A %s attachment named '%s' was skipped" %(content_type, part.get_filename()))
1947                continue
1948
1949            ## Catch some mulitpart execptions
1950            #
1951            if content_type == 'multipart/alternative':
1952                ALTERNATIVE_MULTIPART = True
1953                continue
1954
1955            ## Skip multipart containers
1956            #
1957            if content_maintype == 'multipart':
1958                self.logger.debug("\t Skipping multipart container")
1959                continue
1960           
1961            ## Check if this is an inline part. It's inline if there is co Cont-Disp header,
1962            #  or if there is one and it says "inline"
1963            #
1964            inline = self.inline_part(part)
1965
1966            ## Drop HTML message
1967            #
1968            if ALTERNATIVE_MULTIPART and self.parameters.drop_alternative_html_version:
1969
1970                if content_type == 'text/html':
1971                    self.logger.debug('\t Skipping alternative HTML message')
1972                    ALTERNATIVE_MULTIPART = False
1973                    continue
1974
1975            filename = part.get_filename()
1976            s = '\t unicode filename: %s' %(filename)
1977            self.print_unicode(s)
1978            self.logger.debug('\t raw filename: %s' %repr(filename))
1979
1980            filename = self.check_filename_length(filename)
1981
1982            ## Save all non plain text message as attachment
1983            #
1984            if not content_type in ['text/plain']:
1985
1986                message_parts.append( (filename, part) )
1987
1988                ## We only convert html messages
1989                #
1990                if not content_type == 'text/html':
1991                    self.logger.debug('\t Appending %s (%s)' %(repr(filename), content_type))
1992                    continue
1993
1994
1995            ## We have an text or html message
1996            #
1997            if not inline:
1998                    self.logger.debug('\t Appending %s (%s), not an inline messsage part' %(repr(filename), content_type))
1999                    message_parts.append( (filename, part) )
2000                    continue
2001               
2002            ## Try to decode message part. We have a html or plain text messafe
2003            #
2004            body_text = part.get_payload(decode=1)
2005            if not body_text:           
2006                body_text = part.get_payload(decode=0)
2007
2008            ## Try to convert html message
2009            #
2010            if content_type == 'text/html':
2011
2012                body_text = self.html_2_txt(body_text)
2013                if not body_text:
2014                    continue
2015
2016            format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
2017            delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
2018
2019            if self.parameters.reflow and not self.parameters.verbatim_format and format == 'flowed':
2020                body_text = self.reflow(body_text, delsp == 'yes')
2021   
2022            if self.parameters.strip_signature:
2023                body_text = self.strip_signature(body_text)
2024
2025            if self.parameters.strip_quotes:
2026                body_text = self.strip_quotes(body_text)
2027
2028            if self.parameters.inline_properties:
2029                body_text = self.inline_properties(body_text)
2030
2031            if self.parameters.use_textwrap:
2032                body_text = self.wrap_text(body_text)
2033
2034            ## Get contents charset (iso-8859-15 if not defined in mail headers)
2035            #
2036            charset = part.get_content_charset()
2037            if not charset:
2038                charset = 'iso-8859-15'
2039
2040            try:
2041                ubody_text = unicode(body_text, charset)
2042
2043            except UnicodeError, detail:
2044                ubody_text = unicode(body_text, 'iso-8859-15')
2045
2046            except LookupError, detail:
2047                ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
2048
2049            if self.parameters.verbatim_format:
2050                message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
2051            else:
2052                message_parts.append('%s' %ubody_text)
2053
2054        return message_parts
2055       
2056    def unique_attachment_names(self, message_parts):
2057        """
2058        Make sure we have unique names attachments:
2059          - check if it contains illegal characters
2060          - Rename "None" filenames to "untitled-part"
2061        """
2062        self.logger.debug('function unique_attachment_names()')
2063        renamed_parts = []
2064        attachment_names = set()
2065
2066        for item in message_parts:
2067           
2068            ## If not an attachment, leave it alone
2069            #
2070            if not isinstance(item, tuple):
2071                renamed_parts.append(item)
2072                continue
2073               
2074            (filename, part) = item
2075
2076            ## If filename = None, use a default one
2077            #
2078            if filename in [ 'None']:
2079                filename = 'untitled-part'
2080                self.logger.info('\t Rename filename "None" to: %s' %filename)
2081
2082                ## Guess the extension from the content type, use non strict mode
2083                #  some additional non-standard but commonly used MIME types
2084                #  are also recognized
2085                #
2086                ext = mimetypes.guess_extension(part.get_content_type(), False)
2087                if not ext:
2088                    ext = '.bin'
2089
2090                filename = '%s%s' % (filename, ext)
2091
2092            ## Discard relative paths for windows/unix in attachment names
2093            #
2094            filename = filename.replace('\\', '_')
2095            filename = filename.replace('/', '_')
2096
2097            ## remove linefeed char
2098            #
2099            for forbidden_char in ['\r', '\n']:
2100                filename = filename.replace(forbidden_char,'')
2101
2102            ## We try to normalize the filename to utf-8 NFC if we can.
2103            #  Files uploaded from OS X might be in NFD.
2104            #  Check python version and then try it
2105            #
2106            #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
2107            #   try:
2108            #       filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
2109            #   except TypeError:
2110            #       pass
2111
2112            ## Make the filename unique for this ticket
2113            #
2114            num = 0
2115            unique_filename = filename
2116            dummy_filename, ext = os.path.splitext(filename)
2117
2118            while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
2119                num += 1
2120                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
2121               
2122            s = '\t Attachment with filename %s will be saved as %s' % (filename, unique_filename)
2123            self.print_unicode(s)
2124
2125            attachment_names.add(unique_filename)
2126
2127            renamed_parts.append((filename, unique_filename, part))
2128   
2129        return renamed_parts
2130           
2131           
2132    def attachment_exists(self, filename):
2133
2134        self.logger.debug("function attachment_exists")
2135
2136        s = '\t check if attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
2137        self.print_unicode(s)
2138
2139        ## Do we have a valid ticket id
2140        #
2141        if not self.id:
2142            return False
2143
2144        try:
2145            if self.system == 'discussion':
2146
2147                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s' % (self.id,), filename)
2148
2149            else:
2150
2151                att = attachment.Attachment(self.env, 'ticket', self.id, filename)
2152
2153            return True
2154
2155        except attachment.ResourceNotFound:
2156
2157            return False
2158
2159########## TRAC Ticket Text ###########################################################
2160           
2161    def get_body_text(self, message_parts):
2162        """
2163        """
2164        self.logger.debug('function get_body_text()')
2165
2166        body_text = []
2167       
2168        for part in message_parts:
2169       
2170            ## Plain text part, append it
2171            #
2172            if not isinstance(part, tuple):
2173                body_text.extend(part.strip().splitlines())
2174                body_text.append("")
2175                continue
2176
2177            (original, filename, part) = part
2178            inline = self.inline_part(part)
2179
2180            ## Skip generation of attachment link if html is converted to text
2181            #
2182            if part.get_content_type() == 'text/html' and self.parameters.html2text_cmd and inline:
2183                s = 'Skipping attachment link for html part: %s' %(filename)
2184                self.print_unicode(s)
2185                continue
2186           
2187            if part.get_content_maintype() == 'image' and inline:
2188
2189                if self.system != 'discussion':
2190                    s = 'wiki image link for: %s' %(filename)
2191                    self.print_unicode(s)
2192                    body_text.append('[[Image(%s)]]' % filename)
2193
2194                body_text.append("")
2195
2196            else:
2197
2198                if self.system != 'discussion':
2199
2200                    s = 'wiki attachment link for: %s' %(filename)
2201                    self.print_unicode(s)
2202                    body_text.append('[attachment:"%s"]' % filename)
2203
2204                body_text.append("")
2205
2206        ## Convert list body_texts to string
2207        #
2208        body_text = '\r\n'.join(body_text)
2209        return body_text
2210
2211    def html_mailto_link(self, subject):
2212        """
2213        This function returns a HTML mailto tag with the ticket id and author email address
2214        """
2215        self.logger.debug("function html_mailto_link")
2216
2217        if not self.author:
2218            author = self.email_addr
2219        else:   
2220            author = self.author
2221
2222        if not self.parameters.mailto_cc:
2223            self.parameters.mailto_cc = ''
2224
2225        ## Bug in urllib.quote function
2226        #
2227        if isinstance(subject, unicode):
2228                subject = subject.encode('utf-8')
2229
2230        ## use urllib to escape the chars
2231        #
2232        s = '%s?Subject=%s&cc=%s' %(
2233               urllib.quote(self.email_addr),
2234               urllib.quote('Re: #%s: %s' %(self.id, subject)),
2235               urllib.quote(self.parameters.mailto_cc)
2236               )
2237
2238        if self.VERSION in [ 0.10 ]:
2239            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)
2240        else:
2241            s = '[mailto:"%s" Reply to: %s]' %(s, author)
2242
2243        self.logger.debug("\tmailto link %s" %s)
2244        return s
2245
2246########## TRAC notify section ###########################################################
2247
2248    def notify(self, tkt, new=True, modtime=0):
2249        """
2250        A wrapper for the TRAC notify function. So we can use templates
2251        """
2252        self.logger.debug('function notify()')
2253
2254        if self.parameters.notify_reporter:
2255            self.logger.debug('\t Notify reporter set')
2256            global sender_email
2257            sender_email = self.email_addr
2258 
2259            self.logger.debug('\t Using Email2TracNotification function AlwaysNotifyReporter')
2260            import trac.notification as Email2TracNotification
2261            Email2TracNotification.Notify.notify = AlwaysNotifyReporter
2262
2263        if self.parameters.dry_run  :
2264                self.logger.info('DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter'])
2265                return
2266        try:
2267
2268            #from trac.ticket.notification import TicketNotificationSystem
2269            #tn_sys = TicketNotificationSystem(self.env)
2270            #print tn_sys
2271            #print tn_sys.__dict__
2272            #sys.exit(0)
2273
2274            ## create false {abs_}href properties, to trick Notify()
2275            #
2276            if self.VERSION in [0.10]:
2277                self.env.abs_href = Href(self.get_config('project', 'url'))
2278                self.env.href = Href(self.get_config('project', 'url'))
2279
2280            tn = TicketNotifyEmail(self.env)
2281
2282            if self.parameters.alternate_notify_template:
2283
2284                if self.VERSION >= 0.11:
2285
2286                    from trac.web.chrome import Chrome
2287
2288                    if  self.parameters.alternate_notify_template_update and not new:
2289                        tn.template_name = self.parameters.alternate_notify_template_update
2290                    else:
2291                        tn.template_name = self.parameters.alternate_notify_template
2292
2293                    tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
2294                       
2295                else:
2296
2297                    tn.template_name = self.parameters.alternate_notify_template
2298
2299            tn.notify(tkt, new, modtime)
2300
2301        except Exception, e:
2302            self.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e))
2303
2304########## END Class Definition  ########################################################
2305
2306########## Global Notificaition Function ################################################
2307def AlwaysNotifyReporter(self, resid):
2308    """
2309    Copy of def notify() to manipulate recipents to always include reporter for the
2310    notification.
2311    """
2312    #print sender_email, resid
2313    (torcpts, ccrcpts) = self.get_recipients(resid)
2314    #print "get_recipients finished"
2315
2316    if not tktparser.email_header_acl('notify_reporter_black_list', sender_email, False):
2317        ## additionally append sender (regardeless of settings in trac.ini)
2318        #
2319        if not sender_email in torcpts:
2320            torcpts.append(sender_email)
2321
2322    self.begin_send()
2323    self.send(torcpts, ccrcpts)
2324    self.finish_send()
2325
2326########## Parse Config File  ###########################################################
2327
2328def ReadConfig(file, name):
2329    """
2330    Parse the config file
2331    """
2332    if not os.path.isfile(file):
2333        print 'File %s does not exist' %file
2334        sys.exit(1)
2335
2336    config = trac_config.Configuration(file)
2337   
2338    parentdir = config.get('DEFAULT', 'parentdir')
2339    sections = config.sections()
2340
2341    ## use some trac internals to get the defaults
2342    #
2343    tmp = config.parser.defaults()
2344    project =  SaraDict()
2345
2346    for option, value in tmp.items():
2347        try:
2348            project[option] = int(value)
2349        except ValueError:
2350            project[option] = value
2351
2352    if name:
2353        if name in sections:
2354            project =  SaraDict()
2355            for option, value in  config.options(name):
2356                try:
2357                    project[option] = int(value)
2358                except ValueError:
2359                    project[option] = value
2360
2361        elif not parentdir:
2362            print "Not a valid project name: %s, valid names are: %s" %(name, sections)
2363            print "or set parentdir in the [DEFAULT] section"
2364            sys.exit(1)
2365
2366    ## If parentdir then set project dir to parentdir + name
2367    #
2368    if not project.has_key('project'):
2369        if not parentdir:
2370            print "You must set project or parentdir in your configuration file"
2371            sys.exit(1)
2372        elif not name:
2373            print "You must configure a  project section in your configuration file"
2374        else:
2375            project['project'] = os.path.join(parentdir, name)
2376
2377    ##
2378    # Save the project name
2379    #
2380    project['project_name'] = os.path.basename(project['project'])
2381
2382    return project
2383
2384########## Setup Logging ###############################################################
2385
2386def setup_log(parameters, project_name, interactive=None):
2387    """
2388    Setup logging
2389
2390    Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
2391    would be interpreted by the ConfigParser itself.
2392    """
2393    logger = logging.getLogger('email2trac %s' %project_name)
2394
2395    if interactive:
2396        parameters.log_type = 'stderr'
2397
2398    if not parameters.log_type:
2399        if sys.platform in ['win32', 'cygwin']:
2400            parameters.log_type = 'eventlog'
2401        else:
2402            parameters.log_type = 'syslog'
2403
2404    if parameters.log_type == 'file':
2405
2406        if not parameters.log_file:
2407            parameters.log_file = 'email2trac.log'
2408
2409        if not os.path.isabs(parameters.log_file):
2410            parameters.log_file = os.path.join(tempfile.gettempdir(), parameters.log_file)
2411
2412        log_handler = logging.FileHandler(parameters.log_file)
2413
2414    elif parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
2415        ## Requires win32 extensions
2416        #
2417        logid = "email2trac"
2418        log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application')
2419
2420    elif parameters.log_type in ('syslog', 'unix'):
2421        log_handler = logging.handlers.SysLogHandler('/dev/log')
2422
2423    elif parameters.log_type in ('stderr'):
2424        log_handler = logging.StreamHandler(sys.stderr)
2425
2426    else:
2427        log_handler = logging.handlers.BufferingHandler(0)
2428
2429    if parameters.log_format:
2430        parameters.log_format = parameters.log_format.replace('$(', '%(')
2431    else:
2432        parameters.log_format = '%(name)s: %(message)s'
2433        if parameters.log_type in ('file', 'stderr'):
2434            parameters.log_format = '%(asctime)s ' + parameters.log_format
2435
2436    log_formatter = logging.Formatter(parameters.log_format)
2437    log_handler.setFormatter(log_formatter)
2438    logger.addHandler(log_handler)
2439
2440    if (parameters.log_level in ['DEBUG', 'ALL']) or (parameters.debug > 0):
2441        logger.setLevel(logging.DEBUG)
2442        parameters.debug = 1
2443
2444    elif parameters.log_level in ['INFO'] or parameters.verbose:
2445        logger.setLevel(logging.INFO)
2446
2447    elif parameters.log_level in ['WARNING']:
2448        logger.setLevel(logging.WARNING)
2449
2450    elif parameters.log_level in ['ERROR']:
2451        logger.setLevel(logging.ERROR)
2452
2453    elif parameters.log_level in ['CRITICAL']:
2454        logger.setLevel(logging.CRITICAL)
2455
2456    else:
2457        logger.setLevel(logging.INFO)
2458
2459    return logger
2460
2461
2462if __name__ == '__main__':
2463    ## Default config file
2464    #
2465    configfile = '@email2trac_conf@'
2466    project = ''
2467    component = ''
2468    ticket_prefix = 'default'
2469    dry_run = None
2470    verbose = None
2471    debug_interactive = None
2472
2473    SHORT_OPT = 'cdhf:np:t:v'
2474    LONG_OPT  =  ['component=', 'debug', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
2475
2476    try:
2477        opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
2478    except getopt.error,detail:
2479        print __doc__
2480        print detail
2481        sys.exit(1)
2482   
2483    project_name = None
2484    for opt,value in opts:
2485        if opt in [ '-h', '--help']:
2486            print __doc__
2487            sys.exit(0)
2488        elif opt in ['-c', '--component']:
2489            component = value
2490        elif opt in ['-d', '--debug']:
2491            debug_interactive = 1
2492        elif opt in ['-f', '--file']:
2493            configfile = value
2494        elif opt in ['-n', '--dry-run']:
2495            dry_run = True
2496        elif opt in ['-p', '--project']:
2497            project_name = value
2498        elif opt in ['-t', '--ticket_prefix']:
2499            ticket_prefix = value
2500        elif opt in ['-v', '--verbose']:
2501            verbose = True
2502   
2503    settings = ReadConfig(configfile, project_name)
2504
2505    ## The default prefix for ticket values in email2trac.conf
2506    #
2507    settings.ticket_prefix = ticket_prefix
2508    settings.dry_run = dry_run
2509    settings.verbose = verbose
2510
2511    if not settings.debug and debug_interactive:
2512        settings.debug = debug_interactive
2513
2514    if not settings.project:
2515        print __doc__
2516        print 'No Trac project is defined in the email2trac config file.'
2517        sys.exit(1)
2518
2519    logger = setup_log(settings, os.path.basename(settings.project), debug_interactive)
2520   
2521    if component:
2522        settings['component'] = component
2523
2524    ## Determine major trac version used to be in email2trac.conf
2525    # Quick hack for 0.12
2526    #
2527    version = '0.%s' %(trac_version.split('.')[1])
2528    if version.startswith('0.12'):
2529        version = '0.12'
2530    elif version.startswith('0.13'):
2531        version = '0.13'
2532
2533    logger.debug("Found trac version: %s" %(version))
2534   
2535    try:
2536        if version == '0.10':
2537            from trac import attachment
2538            from trac.env import Environment
2539            from trac.ticket import Ticket
2540            from trac.web.href import Href
2541            from trac import util
2542            from trac.ticket.web_ui import TicketModule
2543
2544            #
2545            # return  util.text.to_unicode(str)
2546            #
2547            # see http://projects.edgewall.com/trac/changeset/2799
2548            from trac.ticket.notification import TicketNotifyEmail
2549            from trac import config as trac_config
2550            from trac.core import TracError
2551
2552        elif version in ['0.11', '0.12', '0.13']:
2553            from trac import attachment
2554            from trac import config as trac_config
2555            from trac import util
2556            from trac.core import TracError
2557            from trac.env import Environment
2558            from trac.perm import PermissionSystem
2559            from trac.perm import PermissionCache
2560            from trac.test import Mock, MockPerm
2561            from trac.ticket import Ticket
2562            from trac.ticket.api import TicketSystem
2563            from trac.ticket.web_ui import TicketModule
2564            from trac.web.href import Href
2565
2566            #
2567            # return  util.text.to_unicode(str)
2568            #
2569            # see http://projects.edgewall.com/trac/changeset/2799
2570            from trac.ticket.notification import TicketNotifyEmail
2571
2572        else:
2573            logger.error('TRAC version %s is not supported' %version)
2574            sys.exit(0)
2575
2576        ## Must be set before environment is created
2577        #
2578        if settings.has_key('python_egg_cache'):
2579            python_egg_cache = str(settings['python_egg_cache'])
2580            os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
2581
2582        if settings.debug > 0:
2583            logger.debug('Loading environment %s', settings.project)
2584
2585        try:
2586            env = Environment(settings['project'], create=0)
2587        except IOError, detail:
2588            logger.error("trac error: %s" %detail)
2589            sys.exit(0)
2590
2591        tktparser = TicketEmailParser(env, settings, logger, float(version))
2592        tktparser.parse(sys.stdin)
2593
2594    ## Catch all errors and use the logging module
2595    #
2596    except Exception, error:
2597
2598        etype, evalue, etb = sys.exc_info()
2599        for e in traceback.format_exception(etype, evalue, etb):
2600            logger.critical(e)
2601
2602        if m:
2603            tktparser.save_email_for_debug(m, settings.project_name, True)
2604
2605        sys.exit(1)
2606# EOB
Note: See TracBrowser for help on using the repository browser.