source: trunk/email2trac.py.in @ 347

Last change on this file since 347 was 347, checked in by bas, 14 years ago

email2trac.py.in:

  • re-enabled normalizing for filenames (attachments)
  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 44.4 KB
Line 
1#!@PYTHON@
2# Copyright (C) 2002
3#
4# This file is part of the email2trac utils
5#
6# This program is free software; you can redistribute it and/or modify it
7# under the terms of the GNU General Public License as published by the
8# Free Software Foundation; either version 2, or (at your option) any
9# later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
19#
20# For vi/emacs or other use tabstop=4 (vi: set ts=4)
21#
22"""
23email2trac.py -- Email tickets to Trac.
24
25A simple MTA filter to create Trac tickets from inbound emails.
26
27Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28Copyright 2005, Edgewall Software
29
30Authors:
31  Bas van der Vlies <basv@sara.nl>
32  Walter de Jong <walter@sara.nl>
33
34The scripts reads emails from stdin and inserts directly into a Trac database.
35
36How to use
37----------
38 * See https://subtrac.sara.nl/oss/email2trac/
39
40 * Create an config file:
41    [DEFAULT]                        # REQUIRED
42    project      : /data/trac/test   # REQUIRED
43    debug        : 1                 # OPTIONAL, if set print some DEBUG info
44    trac_version : 0.10              # OPTIONAL, default is 0.11
45
46    [jouvin]                         # OPTIONAL project declaration, if set both fields necessary
47    project      : /data/trac/jouvin # use -p|--project jouvin. 
48       
49 * default config file is : /etc/email2trac.conf
50
51 * Commandline opions:
52                -h,--help
53                -f,--file  <configuration file>
54                -n,--dry-run
55                -p, --project <project name>
56                -t, --ticket_prefix <name>
57
58SVN Info:
59        $Id: email2trac.py.in 347 2010-04-12 13:32:46Z bas $
60"""
61import os
62import sys
63import string
64import getopt
65import stat
66import time
67import email
68import email.Iterators
69import email.Header
70import re
71import urllib
72import unicodedata
73from stat import *
74import mimetypes
75import traceback
76
77
78# Will fail where unavailable, e.g. Windows
79#
80try:
81    import syslog
82    SYSLOG_AVAILABLE = True
83except ImportError:
84    SYSLOG_AVAILABLE = False
85
86from datetime import tzinfo, timedelta, datetime
87from trac import config as trac_config
88
89# Some global variables
90#
91trac_default_version = '0.11'
92m = None
93
94# A UTC class needed for trac version 0.11, added by
95# tbaschak at ktc dot mb dot ca
96#
97class UTC(tzinfo):
98        """UTC"""
99        ZERO = timedelta(0)
100        HOUR = timedelta(hours=1)
101       
102        def utcoffset(self, dt):
103                return self.ZERO
104               
105        def tzname(self, dt):
106                return "UTC"
107               
108        def dst(self, dt):
109                return self.ZERO
110
111
112class TicketEmailParser(object):
113        env = None
114        comment = '> '
115   
116        def __init__(self, env, parameters, version):
117                self.env = env
118
119                # Database connection
120                #
121                self.db = None
122
123                # Save parameters
124                #
125                self.parameters = parameters
126
127                # Some useful mail constants
128                #
129                self.email_name = None
130                self.email_addr = None
131                self.email_from = None
132                self.author     = None
133                self.id         = None
134               
135                self.STRIP_CONTENT_TYPES = list()
136
137                self.VERSION = version
138                self.DRY_RUN = parameters['dry_run']
139                self.VERBOSE = parameters['verbose']
140
141                self.get_config = self.env.config.get
142
143                if parameters.has_key('umask'):
144                        os.umask(int(parameters['umask'], 8))
145
146                if parameters.has_key('debug'):
147                        self.DEBUG = int(parameters['debug'])
148                else:
149                        self.DEBUG = 0
150
151                if parameters.has_key('mailto_link'):
152                        self.MAILTO = int(parameters['mailto_link'])
153                        if parameters.has_key('mailto_cc'):
154                                self.MAILTO_CC = parameters['mailto_cc']
155                        else:
156                                self.MAILTO_CC = ''
157                else:
158                        self.MAILTO = 0
159
160                if parameters.has_key('spam_level'):
161                        self.SPAM_LEVEL = int(parameters['spam_level'])
162                else:
163                        self.SPAM_LEVEL = 0
164
165                if parameters.has_key('spam_header'):
166                        self.SPAM_HEADER = parameters['spam_header']
167                else:
168                        self.SPAM_HEADER = 'X-Spam-Score'
169
170                if parameters.has_key('email_quote'):
171                        self.EMAIL_QUOTE = str(parameters['email_quote'])
172                else:   
173                        self.EMAIL_QUOTE = '> '
174
175                if parameters.has_key('email_header'):
176                        self.EMAIL_HEADER = int(parameters['email_header'])
177                else:
178                        self.EMAIL_HEADER = 0
179
180                if parameters.has_key('alternate_notify_template'):
181                        self.notify_template = str(parameters['alternate_notify_template'])
182                else:
183                        self.notify_template = None
184
185                if parameters.has_key('alternate_notify_template_update'):
186                        self.notify_template_update = str(parameters['alternate_notify_template_update'])
187                else:
188                        self.notify_template_update = None
189
190                if parameters.has_key('reply_all'):
191                        self.REPLY_ALL = int(parameters['reply_all'])
192                else:
193                        self.REPLY_ALL = 0
194
195                if parameters.has_key('ticket_update'):
196                        self.TICKET_UPDATE = int(parameters['ticket_update'])
197                else:
198                        self.TICKET_UPDATE = 0
199
200                if parameters.has_key('drop_spam'):
201                        self.DROP_SPAM = int(parameters['drop_spam'])
202                else:
203                        self.DROP_SPAM = 0
204
205                if parameters.has_key('verbatim_format'):
206                        self.VERBATIM_FORMAT = int(parameters['verbatim_format'])
207                else:
208                        self.VERBATIM_FORMAT = 1
209
210                if parameters.has_key('reflow'):
211                        self.REFLOW = int(parameters['reflow'])
212                else:
213                        self.REFLOW = 1
214
215                if parameters.has_key('drop_alternative_html_version'):
216                        self.DROP_ALTERNATIVE_HTML_VERSION = int(parameters['drop_alternative_html_version'])
217                else:
218                        self.DROP_ALTERNATIVE_HTML_VERSION = 0
219
220                if parameters.has_key('strip_signature'):
221                        self.STRIP_SIGNATURE = int(parameters['strip_signature'])
222                else:
223                        self.STRIP_SIGNATURE = 0
224
225                if parameters.has_key('strip_quotes'):
226                        self.STRIP_QUOTES = int(parameters['strip_quotes'])
227                else:
228                        self.STRIP_QUOTES = 0
229
230                self.properties = dict()
231                if parameters.has_key('inline_properties'):
232                        self.INLINE_PROPERTIES = int(parameters['inline_properties'])
233                else:
234                        self.INLINE_PROPERTIES = 0
235
236                if parameters.has_key('use_textwrap'):
237                        self.USE_TEXTWRAP = int(parameters['use_textwrap'])
238                else:
239                        self.USE_TEXTWRAP = 0
240
241                if parameters.has_key('binhex'):
242                        self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
243
244                if parameters.has_key('applesingle'):
245                        self.STRIP_CONTENT_TYPES.append('application/applefile')
246
247                if parameters.has_key('appledouble'):
248                        self.STRIP_CONTENT_TYPES.append('application/applefile')
249
250                if parameters.has_key('strip_content_types'):
251                        items = parameters['strip_content_types'].split(',')
252                        for item in items:
253                                self.STRIP_CONTENT_TYPES.append(item.strip())
254
255                self.WORKFLOW = None
256                if parameters.has_key('workflow'):
257                        self.WORKFLOW = parameters['workflow']
258
259                # Use OS independend functions
260                #
261                self.TMPDIR = os.path.normcase('/tmp')
262                if parameters.has_key('tmpdir'):
263                        self.TMPDIR = os.path.normcase(str(parameters['tmpdir']))
264
265                if parameters.has_key('ignore_trac_user_settings'):
266                        self.IGNORE_TRAC_USER_SETTINGS = int(parameters['ignore_trac_user_settings'])
267                else:
268                        self.IGNORE_TRAC_USER_SETTINGS = 0
269
270                if parameters.has_key('email_triggers_workflow'):
271                        self.EMAIL_TRIGGERS_WORKFLOW = int(parameters['email_triggers_workflow'])
272                else:
273                        self.EMAIL_TRIGGERS_WORKFLOW = 1
274
275                if parameters.has_key('subject_field_separator'):
276                        self.SUBJECT_FIELD_SEPARATOR = parameters['subject_field_separator'].strip()
277                else:
278                        self.SUBJECT_FIELD_SEPARATOR = '&'
279
280                self.trac_smtp_from = self.get_config('notification', 'smtp_from')
281
282########## Email Header Functions ###########################################################
283
284        def spam(self, message):
285                """
286                # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
287                # Note if Spam_level then '*' are included
288                """
289                spam = False
290                if message.has_key(self.SPAM_HEADER):
291                        spam_l = string.split(message[self.SPAM_HEADER])
292
293                        try:
294                                number = spam_l[0].count('*')
295                        except IndexError, detail:
296                                number = 0
297                               
298                        if number >= self.SPAM_LEVEL:
299                                spam = True
300                               
301                # treat virus mails as spam
302                #
303                elif message.has_key('X-Virus-found'):                 
304                        spam = True
305
306                # How to handle SPAM messages
307                #
308                if self.DROP_SPAM and spam:
309                        if self.DEBUG > 2 :
310                                print 'This message is a SPAM. Automatic ticket insertion refused (SPAM level > %d' % self.SPAM_LEVEL
311
312                        return 'drop'   
313
314                elif spam:
315
316                        return 'Spam'   
317
318                else:
319
320                        return False
321
322        def email_header_acl(self, keyword, header_field, default):
323                """
324                This function wil check if the email address is allowed or denied
325                to send mail to the ticket list
326            """
327                try:
328                        mail_addresses = self.parameters[keyword]
329
330                        # Check if we have an empty string
331                        #
332                        if not mail_addresses:
333                                return default
334
335                except KeyError, detail:
336                        if self.DEBUG > 2 :
337                                print 'TD: %s not defined, all messages are allowed.' %(keyword)
338
339                        return default
340
341                mail_addresses = string.split(mail_addresses, ',')
342
343                for entry in mail_addresses:
344                        entry = entry.strip()
345                        TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
346                        result =  TO_RE.search(header_field)
347                        if result:
348                                return True
349
350                return False
351
352        def email_header_txt(self, m):
353                """
354                Display To and CC addresses in description field
355                """
356                s = ''
357
358                if m['To'] and len(m['To']) > 0:
359                        s = "'''To:''' %s\r\n" %(m['To'])
360                if m['Cc'] and len(m['Cc']) > 0:
361                        s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
362
363                return  self.email_to_unicode(s)
364
365
366        def get_sender_info(self, message):
367                """
368                Get the default author name and email address from the message
369                """
370
371                self.email_to = self.email_to_unicode(message['to'])
372                self.to_name, self.to_email_addr = email.Utils.parseaddr (self.email_to)
373
374                self.email_from = self.email_to_unicode(message['from'])
375                self.email_name, self.email_addr  = email.Utils.parseaddr(self.email_from)
376
377                ## Trac can not handle author's name that contains spaces
378                #  and forbid the ticket email address as author field
379
380                if self.email_addr == self.trac_smtp_from:
381                        self.author = "email2trac"
382                else:
383                        self.author = self.email_addr
384
385                if self.IGNORE_TRAC_USER_SETTINGS:
386                        return
387
388                # Is this a registered user, use email address as search key:
389                # result:
390                #   u : login name
391                #   n : Name that the user has set in the settings tab
392                #   e : email address that the user has set in the settings tab
393                #
394                users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
395                        if e and (e.lower() == self.email_addr.lower()) ]
396
397                if len(users) == 1:
398                        self.email_from = users[0][0]
399                        self.author = users[0][0]
400
401        def set_reply_fields(self, ticket, message):
402                """
403                Set all the right fields for a new ticket
404                """
405                if self.DEBUG:
406                        print 'TD: set_reply_fields'
407
408                ## Only use name or email adress
409                #ticket['reporter'] = self.email_from
410                ticket['reporter'] = self.author
411
412
413                # Put all CC-addresses in ticket CC field
414                #
415                if self.REPLY_ALL:
416
417                        email_cc = ''
418
419                        cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
420
421                        if not cc_addrs:
422                                return
423
424                        ## Build a list of forbidden CC addresses
425                        #
426                        #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
427                        #to_list = list()
428                        #for n,e in to_addrs:
429                        #       to_list.append(e)
430                               
431                        # Remove reporter email address if notification is
432                        # on
433                        #
434                        if self.notification:
435                                try:
436                                        cc_addrs.remove((self.author, self.email_addr))
437                                except ValueError, detail:
438                                        pass
439
440                        for name,addr in cc_addrs:
441               
442                                ## Prevent mail loop
443                                #
444                                #if addr in to_list:
445
446                                if addr == self.trac_smtp_from:
447                                        if self.DEBUG:
448                                                print "Skipping %s mail address for CC-field" %(addr)
449                                        continue
450
451                                if email_cc:
452                                        email_cc = '%s, %s' %(email_cc, addr)
453                                else:
454                                        email_cc = addr
455
456                        if email_cc:
457                                if self.DEBUG:
458                                        print 'TD: set_reply_fields: %s' %email_cc
459
460                                ticket['cc'] = self.email_to_unicode(email_cc)
461
462
463########## DEBUG functions  ###########################################################
464
465        def debug_body(self, message_body, tempfile=False):
466                if tempfile:
467                        import tempfile
468                        body_file = tempfile.mktemp('.email2trac')
469                else:
470                        body_file = os.path.join(self.TMPDIR, 'body.txt')
471
472                if self.DRY_RUN:
473                        print 'DRY-RUN: not saving body to %s' %(body_file)
474                        return
475
476                print 'TD: writing body to %s' %(body_file)
477                fx = open(body_file, 'wb')
478                if not message_body:
479                                message_body = '(None)'
480
481                message_body = message_body.encode('utf-8')
482                #message_body = unicode(message_body, 'iso-8859-15')
483
484                fx.write(message_body)
485                fx.close()
486                try:
487                        os.chmod(body_file,S_IRWXU|S_IRWXG|S_IRWXO)
488                except OSError:
489                        pass
490
491        def debug_attachments(self, message_parts):
492                """
493                """
494                if self.VERBOSE:
495                        print "VB: debug_attachments"
496               
497                n = 0
498                for item in message_parts:
499                        # Skip inline text parts
500                        if not isinstance(item, tuple):
501                                continue
502                               
503                        (original, filename, part) = item
504
505                        n = n + 1
506                        print 'TD: part%d: Content-Type: %s' % (n, part.get_content_type())
507                        print 'TD: part%d: filename: %s' % (n, part.get_filename())
508
509                        part_file = os.path.join(self.TMPDIR, part.get_filename())
510                        print 'TD: writing part%d (%s)' % (n,part_file)
511
512                        if self.DRY_RUN:
513                                print 'DRY_RUN: NOT saving attachments'
514                                continue
515
516                        fx = open(part_file, 'wb')
517                        text = part.get_payload(decode=1)
518
519                        if not text:
520                                text = '(None)'
521
522                        fx.write(text)
523                        fx.close()
524
525                        try:
526                                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
527                        except OSError:
528                                pass
529
530        def save_email_for_debug(self, message, tempfile=False):
531
532                if tempfile:
533                        import tempfile
534                        msg_file = tempfile.mktemp('.email2trac')
535                else:
536                        #msg_file = '/var/tmp/msg.txt'
537                        msg_file = os.path.join(self.TMPDIR, 'msg.txt')
538
539                if self.DRY_RUN:
540                        print 'DRY_RUN: NOT saving email message to %s' %(msg_file)
541                else:
542                        print 'TD: saving email to %s' %(msg_file)
543
544                        fx = open(msg_file, 'wb')
545                        fx.write('%s' % message)
546                        fx.close()
547                       
548                        try:
549                                os.chmod(msg_file,S_IRWXU|S_IRWXG|S_IRWXO)
550                        except OSError:
551                                pass
552
553                message_parts = self.get_message_parts(message)
554                message_parts = self.unique_attachment_names(message_parts)
555                body_text = self.body_text(message_parts)
556                self.debug_body(body_text, True)
557                self.debug_attachments(message_parts)
558
559########## Conversion functions  ###########################################################
560
561        def email_to_unicode(self, message_str):
562                """
563                Email has 7 bit ASCII code, convert it to unicode with the charset
564                that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
565                understands it.
566                """
567                if self.VERBOSE:
568                        print "VB: email_to_unicode"
569
570                results =  email.Header.decode_header(message_str)
571
572                s = None
573                for text,format in results:
574                        if format:
575                                try:
576                                        temp = unicode(text, format)
577                                except UnicodeError, detail:
578                                        # This always works
579                                        #
580                                        temp = unicode(text, 'iso-8859-15')
581                                except LookupError, detail:
582                                        #text = 'ERROR: Could not find charset: %s, please install' %format
583                                        #temp = unicode(text, 'iso-8859-15')
584                                        temp = message_str
585                                       
586                        else:
587                                temp = string.strip(text)
588                                temp = unicode(text, 'iso-8859-15')
589
590                        if s:
591                                s = '%s %s' %(s, temp)
592                        else:
593                                s = '%s' %temp
594
595                #s = s.encode('utf-8')
596                return s
597
598        def str_to_dict(self, s):
599                """
600                Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
601                """
602
603                fields = string.split(s, self.SUBJECT_FIELD_SEPARATOR)
604
605                result = dict()
606                for field in fields:
607                        try:
608                                index, value = string.split(field, '=')
609
610                                # We can not change the description of a ticket via the subject
611                                # line. The description is the body of the email
612                                #
613                                if index.lower() in ['description']:
614                                        continue
615
616                                if value:
617                                        result[index.lower()] = value
618
619                        except ValueError:
620                                pass
621                return result
622
623########## TRAC ticket functions  ###########################################################
624
625        def update_ticket_fields(self, ticket, user_dict, use_default=None):
626                """
627                This will update the ticket fields. It will check if the
628                given fields are known and if the right values are specified
629                It will only update the ticket field value:
630                        - If the field is known
631                        - If the value supplied is valid for the ticket field.
632                          If not then there are two options:
633                           1) Skip the value (use_default=None)
634                           2) Set default value for field (use_default=1)
635                """
636                if self.VERBOSE:
637                        print "VB: update_ticket_fields"
638
639                # Build a system dictionary from the ticket fields
640                # with field as index and option as value
641                #
642                sys_dict = dict()
643                for field in ticket.fields:
644                        try:
645                                sys_dict[field['name']] = field['options']
646
647                        except KeyError:
648                                sys_dict[field['name']] = None
649                                pass
650
651                ## Check user supplied fields an compare them with the
652                # system one's
653                #
654                for field,value in user_dict.items():
655                        if self.DEBUG >= 10:
656                                print  'user_field\t %s = %s' %(field,value)
657
658                        ## To prevent mail loop
659                        #
660                        if field == 'cc':
661
662                                cc_list = user_dict['cc'].split(',')
663
664                                if self.trac_smtp_from in cc_list:
665                                        if self.DEBUG > 10:
666                                                print 'TD: MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from)
667                                        cc_list.remove(self.trac_smtp_from)
668
669                                value = ','.join(cc_list)
670                               
671
672                        if sys_dict.has_key(field):
673
674                                # Check if value is an allowed system option, if TypeError then
675                                # every value is allowed
676                                #
677                                try:
678                                        if value in sys_dict[field]:
679                                                ticket[field] = value
680                                        else:
681                                                # Must we set a default if value is not allowed
682                                                #
683                                                if use_default:
684                                                        value = self.get_config('ticket', 'default_%s' %(field) )
685
686                                except TypeError:
687                                        pass
688
689                                ## Only set if we have a value
690                                #
691                                if value:
692                                        ticket[field] = value
693
694                                if self.DEBUG >= 10:
695                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
696
697        def ticket_update(self, m, id, spam):
698                """
699                If the current email is a reply to an existing ticket, this function
700                will append the contents of this email to that ticket, instead of
701                creating a new one.
702                """
703                if self.VERBOSE:
704                        print "VB: ticket_update: %s" %id
705
706                # Must we update ticket fields
707                #
708                update_fields = dict()
709                try:
710                        id, keywords = string.split(id, '?')
711
712                        # Skip the last ':' character
713                        #
714                        keywords = keywords[:-1]
715                        update_fields = self.str_to_dict(keywords)
716
717                        # Strip '#'
718                        #
719                        self.id = int(id[1:])
720
721                except ValueError:
722                        # Strip '#' and ':'
723                        #
724                        self.id = int(id[1:-1])
725
726
727                # When is the change committed
728                #
729                if self.VERSION == 0.11:
730                        utc = UTC()
731                        when = datetime.now(utc)
732                else:
733                        when = int(time.time())
734
735                try:
736                        tkt = Ticket(self.env, self.id, self.db)
737                except util.TracError, detail:
738                        # Not a valid ticket
739                        self.id = None
740                        return False
741
742                # How many changes has this ticket
743                cnum = len(tkt.get_changelog())
744
745
746                # reopen the ticket if it is was closed
747                # We must use the ticket workflow framework
748                #
749                if tkt['status'] in ['closed'] and self.EMAIL_TRIGGERS_WORKFLOW:
750
751                        #print controller.actions['reopen']
752                        #
753                        # As reference 
754                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
755                        #
756                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
757                        #print 'controller : ', a
758                        #
759                        #b = controller.get_all_status()
760                        #print 'get all status: ', b
761                        #
762                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
763                        #print 'get_ticket_changes :', b
764
765                        if self.WORKFLOW and (self.VERSION in [0.11]) :
766                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
767                                from trac.test import Mock, MockPerm
768
769                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
770
771                                controller = ConfigurableTicketWorkflow(self.env)
772                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
773
774                                if self.DEBUG:
775                                        print 'TD: Workflow ticket update fields: ', fields
776
777                                for key in fields.keys():
778                                        tkt[key] = fields[key]
779
780                        else:
781                                tkt['status'] = 'reopened'
782                                tkt['resolution'] = ''
783
784                # Must we update some ticket fields properties via subjectline
785                #
786                if update_fields:
787                        self.update_ticket_fields(tkt, update_fields)
788
789                message_parts = self.get_message_parts(m)
790                message_parts = self.unique_attachment_names(message_parts)
791
792                # Must we update some ticket fields properties via body_text
793                #
794                if self.properties:
795                                self.update_ticket_fields(tkt, self.properties)
796
797                if self.EMAIL_HEADER:
798                        message_parts.insert(0, self.email_header_txt(m))
799
800                body_text = self.body_text(message_parts)
801
802                if body_text.strip() or update_fields or self.properties:
803                        if self.DRY_RUN:
804                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
805                        else:
806                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
807                       
808
809                if self.VERSION  == 0.9:
810                        error_with_attachments = self.attach_attachments(message_parts, True)
811                else:
812                        error_with_attachments = self.attach_attachments(message_parts)
813
814                if self.notification and not spam:
815                        self.notify(tkt, False, when)
816
817                return True
818
819        def set_ticket_fields(self, ticket):
820                """
821                set the ticket fields to value specified
822                        - /etc/email2trac.conf with <prefix>_<field>
823                        - trac default values, trac.ini
824                """
825                user_dict = dict()
826
827                for field in ticket.fields:
828
829                        name = field['name']
830
831                        ## skip some fields like resolution
832                        #
833                        if name in [ 'resolution' ]:
834                                continue
835
836                        ## default trac value
837                        #
838                        if not field.get('custom'):
839                                value = self.get_config('ticket', 'default_%s' %(name) )
840                        else:
841                                ##  Else we get the default value for reporter
842                                #
843                                value = field.get('value')
844                                options = field.get('options')
845
846                                if value and options and (value not in options):
847                                         value = options[int(value)]
848       
849                        if self.DEBUG > 10:
850                                print 'trac.ini name %s = %s' %(name, value)
851
852                        ## email2trac.conf settings
853                        #
854                        prefix = self.parameters['ticket_prefix']
855                        try:
856                                value = self.parameters['%s_%s' %(prefix, name)]
857                                if self.DEBUG > 10:
858                                        print 'email2trac.conf %s = %s ' %(name, value)
859
860                        except KeyError, detail:
861                                pass
862               
863                        if self.DEBUG:
864                                print 'user_dict[%s] = %s' %(name, value)
865
866                        if value:
867                                user_dict[name] = value
868
869                self.update_ticket_fields(ticket, user_dict, use_default=1)
870
871                ## Set status ticket
872                #
873                ticket['status'] = 'new'
874
875
876
877        def new_ticket(self, msg, subject, spam, set_fields = None):
878                """
879                Create a new ticket
880                """
881                if self.DEBUG:
882                        print "TD: new_ticket"
883
884                tkt = Ticket(self.env)
885
886                self.set_reply_fields(tkt, msg)
887
888                self.set_ticket_fields(tkt)
889
890                # Old style setting for component, will be removed
891                #
892                if spam:
893                        tkt['component'] = 'Spam'
894
895                elif self.parameters.has_key('component'):
896                        tkt['component'] = self.parameters['component']
897
898                if not msg['Subject']:
899                        tkt['summary'] = u'(No subject)'
900                else:
901                        tkt['summary'] = subject
902
903
904                if set_fields:
905                        rest, keywords = string.split(set_fields, '?')
906
907                        if keywords:
908                                update_fields = self.str_to_dict(keywords)
909                                self.update_ticket_fields(tkt, update_fields)
910
911                # produce e-mail like header
912                #
913                head = ''
914                if self.EMAIL_HEADER > 0:
915                        head = self.email_header_txt(msg)
916
917                message_parts = self.get_message_parts(msg)
918
919                # Must we update some ticket fields properties via body_text
920                #
921                if self.properties:
922                                self.update_ticket_fields(tkt, self.properties)
923
924                if self.DEBUG:
925                        print 'TD: self.get_message_parts ',
926                        print message_parts
927
928                message_parts = self.unique_attachment_names(message_parts)
929                if self.DEBUG:
930                        print 'TD: self.unique_attachment_names',
931                        print message_parts
932               
933                if self.EMAIL_HEADER > 0:
934                        message_parts.insert(0, self.email_header_txt(msg))
935                       
936                body_text = self.body_text(message_parts)
937
938                tkt['description'] = body_text
939
940                #when = int(time.time())
941                #
942                utc = UTC()
943                when = datetime.now(utc)
944
945                if not self.DRY_RUN:
946                        self.id = tkt.insert()
947       
948                changed = False
949                comment = ''
950
951                # some routines in trac are dependend on ticket id     
952                # like alternate notify template
953                #
954                if self.notify_template:
955                        tkt['id'] = self.id
956                        changed = True
957
958                ## Rewrite the description if we have mailto enabled
959                #
960                if self.MAILTO:
961                        changed = True
962                        comment = u'\nadded mailto line\n'
963                        mailto = self.html_mailto_link( m['Subject'])
964
965                        tkt['description'] = u'%s\r\n%s%s\r\n' \
966                                %(head, mailto, body_text)
967       
968                ## Save the attachments to the ticket   
969                #
970                error_with_attachments =  self.attach_attachments(message_parts)
971
972                if error_with_attachments:
973                        changed = True
974                        comment = '%s\n%s\n' %(comment, error_with_attachments)
975
976                if changed:
977                        if self.DRY_RUN:
978                                print 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
979                        else:
980                                tkt.save_changes(tkt['reporter'], comment)
981                                #print tkt.get_changelog(self.db, when)
982
983                if self.notification and not spam:
984                        self.notify(tkt, True)
985
986
987        def attach_attachments(self, message_parts, update=False):
988                '''
989                save any attachments as files in the ticket's directory
990                '''
991                if self.VERBOSE:
992                        print "VB: attach_attachments()"
993
994                if self.DRY_RUN:
995                        print "DRY_RUN: no attachments attached to tickets"
996                        return ''
997
998                count = 0
999
1000                # Get Maxium attachment size
1001                #
1002                max_size = int(self.get_config('attachment', 'max_size'))
1003                status   = None
1004               
1005                for item in message_parts:
1006                        # Skip body parts
1007                        if not isinstance(item, tuple):
1008                                continue
1009                               
1010                        (original, filename, part) = item
1011                        #
1012                        # Must be tuneables HvB
1013                        #
1014                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
1015                        text = part.get_payload(decode=1)
1016                        if not text:
1017                                text = '(None)'
1018                        fd.write(text)
1019                        fd.close()
1020
1021                        # get the file_size
1022                        #
1023                        stats = os.lstat(path)
1024                        file_size = stats[stat.ST_SIZE]
1025
1026                        # Check if the attachment size is allowed
1027                        #
1028                        if (max_size != -1) and (file_size > max_size):
1029                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1030                                        %(status, original, file_size, max_size)
1031
1032                                os.unlink(path)
1033                                continue
1034                        else:
1035                                count = count + 1
1036                                       
1037                        # Insert the attachment
1038                        #
1039                        fd = open(path, 'rb')
1040                        att = attachment.Attachment(self.env, 'ticket', self.id)
1041
1042                        # This will break the ticket_update system, the body_text is vaporized
1043                        # ;-(
1044                        #
1045                        if not update:
1046                                att.author = self.author
1047                                att.description = self.email_to_unicode('Added by email2trac')
1048
1049                        att.insert(filename, fd, file_size)
1050
1051                        #except  util.TracError, detail:
1052                        #       print detail
1053
1054                        # Remove the created temporary filename
1055                        #
1056                        fd.close()
1057                        os.unlink(path)
1058
1059                ## return error
1060                #
1061                return status
1062
1063########## Fullblog functions  ###########################################################
1064
1065        def blog(self, id):
1066                """
1067                The blog create/update function
1068                """
1069                # import the modules
1070                #
1071                from tracfullblog.core import FullBlogCore
1072                from tracfullblog.model import BlogPost, BlogComment
1073                from trac.test import Mock, MockPerm
1074
1075                # instantiate blog core
1076                blog = FullBlogCore(self.env)
1077                req = Mock(authname='anonymous', perm=MockPerm(), args={})
1078
1079                if id:
1080
1081                        # update blog
1082                        #
1083                        comment = BlogComment(self.env, id)
1084                        comment.author = self.author
1085
1086                        message_parts = self.get_message_parts(m)
1087                        comment.comment = self.body_text(message_parts)
1088
1089                        blog.create_comment(req, comment)
1090
1091                else:
1092                        # create blog
1093                        #
1094                        import time
1095                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
1096
1097                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
1098                       
1099                        post.author = self.author
1100                        post.title = self.email_to_unicode(m['Subject'])
1101
1102                        message_parts = self.get_message_parts(m)
1103                        post.body = self.body_text(message_parts)
1104                       
1105                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
1106
1107
1108
1109########## MAIN function  ###########################################################
1110
1111        def parse(self, fp):
1112                global m
1113
1114                m = email.message_from_file(fp)
1115               
1116                if not m:
1117                        if self.DEBUG:
1118                                print "TD: This is not a valid email message format"
1119                        return
1120                       
1121                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1122                try:
1123                        m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1124                except AttributeError, detail:
1125                        pass
1126
1127                if self.DEBUG > 1:        # save the entire e-mail message text
1128                        self.save_email_for_debug(m, True)
1129
1130                self.db = self.env.get_db_cnx()
1131                self.get_sender_info(m)
1132
1133                if not self.email_header_acl('white_list', self.email_addr, True):
1134                        if self.DEBUG > 1 :
1135                                print 'Message rejected : %s not in white list' %(self.email_addr)
1136                        return False
1137
1138                if self.email_header_acl('black_list', self.email_addr, False):
1139                        if self.DEBUG > 1 :
1140                                print 'Message rejected : %s in black list' %(self.email_addr)
1141                        return False
1142
1143                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
1144                        if self.DEBUG > 1 :
1145                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
1146                        return False
1147
1148                # If drop the message
1149                #
1150                if self.spam(m) == 'drop':
1151                        return False
1152
1153                elif self.spam(m) == 'spam':
1154                        spam_msg = True
1155                else:
1156                        spam_msg = False
1157
1158                if self.get_config('notification', 'smtp_enabled') in ['true']:
1159                        self.notification = 1
1160                else:
1161                        self.notification = 0
1162
1163
1164                # Check if  FullBlogPlugin is installed
1165                #
1166                blog_enabled = None
1167                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1168                        blog_enabled = True
1169
1170                if not m['Subject']:
1171                        subject  = 'No Subject'
1172                else:
1173                        subject  = self.email_to_unicode(m['Subject'])         
1174
1175                #
1176                # [hic] #1529: Re: LRZ
1177                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
1178                #
1179                TICKET_RE = re.compile(r"""
1180                        (?P<blog>blog:(?P<blog_id>\w*))
1181                        |(?P<new_fields>[#][?].*)
1182                        |(?P<reply>[#][\d]+:)
1183                        |(?P<reply_fields>[#][\d]+\?.*?:)
1184                        """, re.VERBOSE)
1185
1186                # Find out if this is a ticket or a blog
1187                #
1188                result =  TICKET_RE.search(subject)
1189
1190                if result:
1191                        if result.group('blog'):
1192                                if blog_enabled:
1193                                        self.blog(result.group('blog_id'))
1194                                else:
1195                                        if self.DEBUG:
1196                                                print "Fullblog plugin is not installed"
1197                                                return
1198
1199                        # update ticket + fields
1200                        #
1201                        if result.group('reply_fields') and self.TICKET_UPDATE:
1202                                self.ticket_update(m, result.group('reply_fields'), spam_msg)
1203
1204                        # Update ticket
1205                        #
1206                        elif result.group('reply') and self.TICKET_UPDATE:
1207                                self.ticket_update(m, result.group('reply'), spam_msg)
1208
1209                        # New ticket + fields
1210                        #
1211                        elif result.group('new_fields'):
1212                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1213
1214                # Create ticket
1215                #
1216                else:
1217                        self.new_ticket(m, subject, spam_msg)
1218
1219########## BODY TEXT functions  ###########################################################
1220
1221        def strip_signature(self, text):
1222                """
1223                Strip signature from message, inspired by Mailman software
1224                """
1225                body = []
1226                for line in text.splitlines():
1227                        if line == '-- ':
1228                                break
1229                        body.append(line)
1230
1231                return ('\n'.join(body))
1232
1233        def reflow(self, text, delsp = 0):
1234                """
1235                Reflow the message based on the format="flowed" specification (RFC 3676)
1236                """
1237                flowedlines = []
1238                quotelevel = 0
1239                prevflowed = 0
1240
1241                for line in text.splitlines():
1242                        from re import match
1243                       
1244                        # Figure out the quote level and the content of the current line
1245                        m = match('(>*)( ?)(.*)', line)
1246                        linequotelevel = len(m.group(1))
1247                        line = m.group(3)
1248
1249                        # Determine whether this line is flowed
1250                        if line and line != '-- ' and line[-1] == ' ':
1251                                flowed = 1
1252                        else:
1253                                flowed = 0
1254
1255                        if flowed and delsp and line and line[-1] == ' ':
1256                                line = line[:-1]
1257
1258                        # If the previous line is flowed, append this line to it
1259                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1260                                flowedlines[-1] += line
1261                        # Otherwise, start a new line
1262                        else:
1263                                flowedlines.append('>' * linequotelevel + line)
1264
1265                        prevflowed = flowed
1266                       
1267
1268                return '\n'.join(flowedlines)
1269
1270        def strip_quotes(self, text):
1271                """
1272                Strip quotes from message by Nicolas Mendoza
1273                """
1274                body = []
1275                for line in text.splitlines():
1276                        if line.startswith(self.EMAIL_QUOTE):
1277                                continue
1278                        body.append(line)
1279
1280                return ('\n'.join(body))
1281
1282        def inline_properties(self, text):
1283                """
1284                Parse text if we use inline keywords to set ticket fields
1285                """
1286                if self.DEBUG:
1287                        print 'TD: inline_properties function'
1288
1289                properties = dict()
1290                body = list()
1291
1292                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1293
1294                for line in text.splitlines():
1295                        match = INLINE_EXP.match(line)
1296                        if match:
1297                                keyword, value = match.groups()
1298                                self.properties[keyword] = value.strip()
1299                                if self.DEBUG:
1300                                        print "TD: inline properties: %s : %s" %(keyword,value)
1301                        else:
1302                                body.append(line)
1303                               
1304                return '\n'.join(body)
1305
1306
1307        def wrap_text(self, text, replace_whitespace = False):
1308                """
1309                Will break a lines longer then given length into several small
1310                lines of size given length
1311                """
1312                import textwrap
1313
1314                LINESEPARATOR = '\n'
1315                reformat = ''
1316
1317                for s in text.split(LINESEPARATOR):
1318                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1319                        if tmp:
1320                                reformat = '%s\n%s' %(reformat,tmp)
1321                        else:
1322                                reformat = '%s\n' %reformat
1323
1324                return reformat
1325
1326                # Python2.4 and higher
1327                #
1328                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1329                #
1330
1331########## EMAIL attachements functions ###########################################################
1332
1333        def inline_part(self, part):
1334                """
1335                """
1336                if self.VERBOSE:
1337                        print "VB: inline_part()"
1338
1339                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1340
1341        def get_message_parts(self, msg):
1342                """
1343                parses the email message and returns a list of body parts and attachments
1344                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1345                """
1346                if self.VERBOSE:
1347                        print "VB: get_message_parts()"
1348
1349                message_parts = list()
1350       
1351                ALTERNATIVE_MULTIPART = False
1352
1353                for part in msg.walk():
1354                        if self.DEBUG:
1355                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
1356                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
1357
1358                        ## Check content type
1359                        #
1360                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
1361
1362                                if self.DEBUG:
1363                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
1364
1365                                continue
1366
1367                        ## Catch some mulitpart execptions
1368                        #
1369                        if part.get_content_type() == 'multipart/alternative':
1370                                ALTERNATIVE_MULTIPART = True
1371                                continue
1372
1373                        ## Skip multipart containers
1374                        #
1375                        if part.get_content_maintype() == 'multipart':
1376                                if self.DEBUG:
1377                                        print "TD: Skipping multipart container"
1378                                continue
1379                       
1380                        ## Check if this is an inline part. It's inline if there is co Cont-Disp header, or if there is one and it says "inline"
1381                        #
1382                        inline = self.inline_part(part)
1383
1384                        ## Drop HTML message
1385                        #
1386                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1387                                if part.get_content_type() == 'text/html':
1388                                        if self.DEBUG:
1389                                                print "TD: Skipping alternative HTML message"
1390
1391                                        ALTERNATIVE_MULTIPART = False
1392                                        continue
1393
1394                        ## Inline text parts are where the body is
1395                        #
1396                        if part.get_content_type() == 'text/plain' and inline:
1397                                if self.DEBUG:
1398                                        print 'TD:               Inline body part'
1399
1400                                # Try to decode, if fails then do not decode
1401                                #
1402                                body_text = part.get_payload(decode=1)
1403                                if not body_text:                       
1404                                        body_text = part.get_payload(decode=0)
1405
1406                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1407                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
1408
1409                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1410                                        body_text = self.reflow(body_text, delsp == 'yes')
1411       
1412                                if self.STRIP_SIGNATURE:
1413                                        body_text = self.strip_signature(body_text)
1414
1415                                if self.STRIP_QUOTES:
1416                                        body_text = self.strip_quotes(body_text)
1417
1418                                if self.INLINE_PROPERTIES:
1419                                        body_text = self.inline_properties(body_text)
1420
1421                                if self.USE_TEXTWRAP:
1422                                        body_text = self.wrap_text(body_text)
1423
1424                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
1425                                #
1426                                charset = part.get_content_charset()
1427                                if not charset:
1428                                        charset = 'iso-8859-15'
1429
1430                                try:
1431                                        ubody_text = unicode(body_text, charset)
1432
1433                                except UnicodeError, detail:
1434                                        ubody_text = unicode(body_text, 'iso-8859-15')
1435
1436                                except LookupError, detail:
1437                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
1438
1439                                if self.VERBATIM_FORMAT:
1440                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1441                                else:
1442                                        message_parts.append('%s' %ubody_text)
1443                        else:
1444                                if self.DEBUG:
1445                                        try:
1446                                                print 'TD:               Filename: %s' % part.get_filename()
1447                                        except UnicodeEncodeError, detail:
1448                                                print 'TD:               Filename: Can not be printed due to non-ascii characters'
1449
1450                                ## Convert 7-bit filename to 8 bits value
1451                                #
1452                                filename = self.email_to_unicode(part.get_filename())
1453                                message_parts.append((filename, part))
1454
1455                return message_parts
1456               
1457        def unique_attachment_names(self, message_parts):
1458                """
1459                """
1460                renamed_parts = []
1461                attachment_names = set()
1462
1463                for item in message_parts:
1464                       
1465                        ## If not an attachment, leave it alone
1466                        #
1467                        if not isinstance(item, tuple):
1468                                renamed_parts.append(item)
1469                                continue
1470                               
1471                        (filename, part) = item
1472
1473                        ## If no filename, use a default one
1474                        #
1475                        if not filename:
1476                                filename = 'untitled-part'
1477
1478                                # Guess the extension from the content type, use non strict mode
1479                                # some additional non-standard but commonly used MIME types
1480                                # are also recognized
1481                                #
1482                                ext = mimetypes.guess_extension(part.get_content_type(), False)
1483                                if not ext:
1484                                        ext = '.bin'
1485
1486                                filename = '%s%s' % (filename, ext)
1487
1488                        ## Discard relative paths in attachment names
1489                        #
1490                        filename = filename.replace('\\', '/').replace(':', '/')
1491                        filename = os.path.basename(filename)
1492
1493                        #
1494                        # We try to normalize the filename to utf-8 NFC if we can.
1495                        # Files uploaded from OS X might be in NFD.
1496                        # Check python version and then try it
1497                        #
1498                        if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1499                                try:
1500                                        filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1501                                except TypeError:
1502                                        pass
1503
1504                        # Make the filename unique for this ticket
1505                        num = 0
1506                        unique_filename = filename
1507                        dummy_filename, ext = os.path.splitext(filename)
1508
1509                        while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
1510                                num += 1
1511                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
1512                               
1513                        if self.DEBUG:
1514                                try:
1515                                        print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1516                                except UnicodeEncodeError, detail:
1517                                        print 'Filename can not be printed due to non-ascii characters'
1518
1519                        attachment_names.add(unique_filename)
1520
1521                        renamed_parts.append((filename, unique_filename, part))
1522       
1523                return renamed_parts
1524                       
1525                       
1526        def attachment_exists(self, filename):
1527
1528                if self.DEBUG:
1529                        s = 'TD: attachment already exists: Ticket id : '
1530                        try:
1531                                print "%s%s, Filename : %s" %(s, self.id, filename)
1532                        except UnicodeEncodeError, detail:
1533                                print "%s%s, Filename : Can not be printed due to non-ascii characters" %(s, self.id)
1534
1535                # We have no valid ticket id
1536                #
1537                if not self.id:
1538                        return False
1539
1540                try:
1541                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
1542                        return True
1543                except attachment.ResourceNotFound:
1544                        return False
1545
1546########## TRAC Ticket Text ###########################################################
1547                       
1548        def body_text(self, message_parts):
1549                body_text = []
1550               
1551                for part in message_parts:
1552                        # Plain text part, append it
1553                        if not isinstance(part, tuple):
1554                                body_text.extend(part.strip().splitlines())
1555                                body_text.append("")
1556                                continue
1557                               
1558                        (original, filename, part) = part
1559                        inline = self.inline_part(part)
1560                       
1561                        if part.get_content_maintype() == 'image' and inline:
1562                                body_text.append('[[Image(%s)]]' % filename)
1563                                body_text.append("")
1564                        else:
1565                                body_text.append('[attachment:"%s"]' % filename)
1566                                body_text.append("")
1567                               
1568                body_text = '\r\n'.join(body_text)
1569                return body_text
1570
1571        def html_mailto_link(self, subject):
1572                """
1573                This function returns a HTML mailto tag with the ticket id and author email address
1574                """
1575                if not self.author:
1576                        author = self.email_addr
1577                else:   
1578                        author = self.author
1579
1580                # use urllib to escape the chars
1581                #
1582                s = 'mailto:%s?Subject=%s&Cc=%s' %(
1583                       urllib.quote(self.email_addr),
1584                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
1585                           urllib.quote(self.MAILTO_CC)
1586                           )
1587
1588                s = '\r\n{{{\r\n#!html\r\n<a\r\n href="%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author)
1589                return s
1590
1591########## TRAC notify section ###########################################################
1592
1593        def notify(self, tkt, new=True, modtime=0):
1594                """
1595                A wrapper for the TRAC notify function. So we can use templates
1596                """
1597                if self.DRY_RUN:
1598                                print 'DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter']
1599                                return
1600                try:
1601                        # create false {abs_}href properties, to trick Notify()
1602                        #
1603                        if not self.VERSION == 0.11:
1604                                self.env.abs_href = Href(self.get_config('project', 'url'))
1605                                self.env.href = Href(self.get_config('project', 'url'))
1606
1607                        tn = TicketNotifyEmail(self.env)
1608
1609                        if self.notify_template:
1610
1611                                if self.VERSION == 0.11:
1612
1613                                        from trac.web.chrome import Chrome
1614
1615                                        if self.notify_template_update and not new:
1616                                                tn.template_name = self.notify_template_update
1617                                        else:
1618                                                tn.template_name = self.notify_template
1619
1620                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1621                                               
1622                                else:
1623
1624                                        tn.template_name = self.notify_template;
1625
1626                        tn.notify(tkt, new, modtime)
1627
1628                except Exception, e:
1629                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
1630
1631
1632
1633########## Parse Config File  ###########################################################
1634
1635def ReadConfig(file, name):
1636        """
1637        Parse the config file
1638        """
1639        if not os.path.isfile(file):
1640                print 'File %s does not exist' %file
1641                sys.exit(1)
1642
1643        config = trac_config.Configuration(file)
1644
1645        # Use given project name else use defaults
1646        #
1647        if name:
1648                sections = config.sections()
1649                if not name in sections:
1650                        print "Not a valid project name: %s" %name
1651                        print "Valid names: %s" %sections
1652                        sys.exit(1)
1653
1654                project =  dict()
1655                for option, value in  config.options(name):
1656                        project[option] = value
1657
1658        else:
1659                # use some trac internals to get the defaults
1660                #
1661                project = config.parser.defaults()
1662
1663        return project
1664
1665
1666if __name__ == '__main__':
1667        # Default config file
1668        #
1669        configfile = '@email2trac_conf@'
1670        project = ''
1671        component = ''
1672        ticket_prefix = 'default'
1673        dry_run = None
1674        verbose = None
1675
1676        ENABLE_SYSLOG = 0
1677
1678
1679        SHORT_OPT = 'chf:np:t:v'
1680        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
1681
1682        try:
1683                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1684        except getopt.error,detail:
1685                print __doc__
1686                print detail
1687                sys.exit(1)
1688       
1689        project_name = None
1690        for opt,value in opts:
1691                if opt in [ '-h', '--help']:
1692                        print __doc__
1693                        sys.exit(0)
1694                elif opt in ['-c', '--component']:
1695                        component = value
1696                elif opt in ['-f', '--file']:
1697                        configfile = value
1698                elif opt in ['-n', '--dry-run']:
1699                        dry_run = True
1700                elif opt in ['-p', '--project']:
1701                        project_name = value
1702                elif opt in ['-t', '--ticket_prefix']:
1703                        ticket_prefix = value
1704                elif opt in ['-v', '--version']:
1705                        verbose = True
1706       
1707        settings = ReadConfig(configfile, project_name)
1708        if not settings.has_key('project'):
1709                print __doc__
1710                print 'No Trac project is defined in the email2trac config file.'
1711                sys.exit(1)
1712       
1713        if component:
1714                settings['component'] = component
1715
1716        # The default prefix for ticket values in email2trac.conf
1717        #
1718        settings['ticket_prefix'] = ticket_prefix
1719        settings['dry_run'] = dry_run
1720        settings['verbose'] = verbose
1721       
1722        if settings.has_key('trac_version'):
1723                version = settings['trac_version']
1724        else:
1725                version = trac_default_version
1726
1727
1728        #debug HvB
1729        #print settings
1730
1731        try:
1732                if version == '0.9':
1733                        from trac import attachment
1734                        from trac.env import Environment
1735                        from trac.ticket import Ticket
1736                        from trac.web.href import Href
1737                        from trac import util
1738                        from trac.Notify import TicketNotifyEmail
1739                elif version == '0.10':
1740                        from trac import attachment
1741                        from trac.env import Environment
1742                        from trac.ticket import Ticket
1743                        from trac.web.href import Href
1744                        from trac import util
1745                        #
1746                        # return  util.text.to_unicode(str)
1747                        #
1748                        # see http://projects.edgewall.com/trac/changeset/2799
1749                        from trac.ticket.notification import TicketNotifyEmail
1750                        from trac import config as trac_config
1751                elif version == '0.11':
1752                        from trac import attachment
1753                        from trac.env import Environment
1754                        from trac.ticket import Ticket
1755                        from trac.web.href import Href
1756                        from trac import config as trac_config
1757                        from trac import util
1758
1759
1760                        #
1761                        # return  util.text.to_unicode(str)
1762                        #
1763                        # see http://projects.edgewall.com/trac/changeset/2799
1764                        from trac.ticket.notification import TicketNotifyEmail
1765                else:
1766                        print 'TRAC version %s is not supported' %version
1767                        sys.exit(1)
1768                       
1769                if settings.has_key('enable_syslog'):
1770                        if SYSLOG_AVAILABLE:
1771                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1772
1773
1774                # Must be set before environment is created
1775                #
1776                if settings.has_key('python_egg_cache'):
1777                        python_egg_cache = str(settings['python_egg_cache'])
1778                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
1779
1780                env = Environment(settings['project'], create=0)
1781
1782                tktparser = TicketEmailParser(env, settings, float(version))
1783                tktparser.parse(sys.stdin)
1784
1785        # Catch all errors ans log to SYSLOG if we have enabled this
1786        # else stdout
1787        #
1788        except Exception, error:
1789                if ENABLE_SYSLOG:
1790                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1791
1792                        etype, evalue, etb = sys.exc_info()
1793                        for e in traceback.format_exception(etype, evalue, etb):
1794                                syslog.syslog(e)
1795
1796                        syslog.closelog()
1797                else:
1798                        traceback.print_exc()
1799
1800                if m:
1801                        tktparser.save_email_for_debug(m, True)
1802
1803                sys.exit(1)
1804# EOB
Note: See TracBrowser for help on using the repository browser.