source: trunk/email2trac.py.in @ 586

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

added blog patch from Thomas Moschny, closes #287,#235,#175

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