source: trunk/email2trac.py.in @ 343

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

Re-arranged functions per section

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 44.3 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 343 2010-03-24 14:53:38Z 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                                                        ticket[field] = value
686
687                                except TypeError:
688                                        ticket[field] = value
689
690                                if self.DEBUG >= 10:
691                                        print  'ticket_field\t %s = %s' %(field,  ticket[field])
692                                       
693        def ticket_update(self, m, id, spam):
694                """
695                If the current email is a reply to an existing ticket, this function
696                will append the contents of this email to that ticket, instead of
697                creating a new one.
698                """
699                if self.VERBOSE:
700                        print "VB: ticket_update: %s" %id
701
702                # Must we update ticket fields
703                #
704                update_fields = dict()
705                try:
706                        id, keywords = string.split(id, '?')
707
708                        # Skip the last ':' character
709                        #
710                        keywords = keywords[:-1]
711                        update_fields = self.str_to_dict(keywords)
712
713                        # Strip '#'
714                        #
715                        self.id = int(id[1:])
716
717                except ValueError:
718                        # Strip '#' and ':'
719                        #
720                        self.id = int(id[1:-1])
721
722
723                # When is the change committed
724                #
725                if self.VERSION == 0.11:
726                        utc = UTC()
727                        when = datetime.now(utc)
728                else:
729                        when = int(time.time())
730
731                try:
732                        tkt = Ticket(self.env, self.id, self.db)
733                except util.TracError, detail:
734                        # Not a valid ticket
735                        self.id = None
736                        return False
737
738                # How many changes has this ticket
739                cnum = len(tkt.get_changelog())
740
741
742                # reopen the ticket if it is was closed
743                # We must use the ticket workflow framework
744                #
745                if tkt['status'] in ['closed'] and self.EMAIL_TRIGGERS_WORKFLOW:
746
747                        #print controller.actions['reopen']
748                        #
749                        # As reference 
750                        # req = Mock(href=Href('/'), abs_href=Href('http://www.example.com/'), authname='anonymous', perm=MockPerm(), args={})
751                        #
752                        #a = controller.render_ticket_action_control(req, tkt, 'reopen')
753                        #print 'controller : ', a
754                        #
755                        #b = controller.get_all_status()
756                        #print 'get all status: ', b
757                        #
758                        #b = controller.get_ticket_changes(req, tkt, 'reopen')
759                        #print 'get_ticket_changes :', b
760
761                        if self.WORKFLOW and (self.VERSION in [0.11]) :
762                                from trac.ticket.default_workflow import ConfigurableTicketWorkflow
763                                from trac.test import Mock, MockPerm
764
765                                req = Mock(authname='anonymous', perm=MockPerm(), args={})
766
767                                controller = ConfigurableTicketWorkflow(self.env)
768                                fields = controller.get_ticket_changes(req, tkt, self.WORKFLOW)
769
770                                if self.DEBUG:
771                                        print 'TD: Workflow ticket update fields: ', fields
772
773                                for key in fields.keys():
774                                        tkt[key] = fields[key]
775
776                        else:
777                                tkt['status'] = 'reopened'
778                                tkt['resolution'] = ''
779
780                # Must we update some ticket fields properties via subjectline
781                #
782                if update_fields:
783                        self.update_ticket_fields(tkt, update_fields)
784
785                message_parts = self.get_message_parts(m)
786                message_parts = self.unique_attachment_names(message_parts)
787
788                # Must we update some ticket fields properties via body_text
789                #
790                if self.properties:
791                                self.update_ticket_fields(tkt, self.properties)
792
793                if self.EMAIL_HEADER:
794                        message_parts.insert(0, self.email_header_txt(m))
795
796                body_text = self.body_text(message_parts)
797
798                if body_text.strip() or update_fields or self.properties:
799                        if self.DRY_RUN:
800                                print 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) ', self.author, cnum
801                        else:
802                                tkt.save_changes(self.author, body_text, when, None, str(cnum))
803                       
804
805                if self.VERSION  == 0.9:
806                        error_with_attachments = self.attach_attachments(message_parts, True)
807                else:
808                        error_with_attachments = self.attach_attachments(message_parts)
809
810                if self.notification and not spam:
811                        self.notify(tkt, False, when)
812
813                return True
814
815        def set_ticket_fields(self, ticket):
816                """
817                set the ticket fields to value specified
818                        - /etc/email2trac.conf with <prefix>_<field>
819                        - trac default values, trac.ini
820                """
821                user_dict = dict()
822
823                for field in ticket.fields:
824
825                        name = field['name']
826
827                        ## skip some fields like resolution
828                        #
829                        if name in [ 'resolution' ]:
830                                continue
831
832                        ## default trac value
833                        #
834                        if not field.get('custom'):
835                                value = self.get_config('ticket', 'default_%s' %(name) )
836                        else:
837                                value = field.get('value')
838                                options = field.get('options')
839                                if value and options and (value not in options):
840                                        value = options[int(value)]
841
842                        if self.DEBUG > 10:
843                                print 'trac.ini name %s = %s' %(name, value)
844
845                        ## email2trac.conf settings
846                        #
847                        prefix = self.parameters['ticket_prefix']
848                        try:
849                                value = self.parameters['%s_%s' %(prefix, name)]
850                                if self.DEBUG > 10:
851                                        print 'email2trac.conf %s = %s ' %(name, value)
852
853                        except KeyError, detail:
854                                pass
855               
856                        if self.DEBUG:
857                                print 'user_dict[%s] = %s' %(name, value)
858
859                        user_dict[name] = value
860
861                self.update_ticket_fields(ticket, user_dict, use_default=1)
862
863                ## Set status ticket
864                #
865                ticket['status'] = 'new'
866
867
868
869        def new_ticket(self, msg, subject, spam, set_fields = None):
870                """
871                Create a new ticket
872                """
873                if self.DEBUG:
874                        print "TD: new_ticket"
875
876                tkt = Ticket(self.env)
877
878                self.set_reply_fields(tkt, msg)
879
880                self.set_ticket_fields(tkt)
881
882                # Old style setting for component, will be removed
883                #
884                if spam:
885                        tkt['component'] = 'Spam'
886
887                elif self.parameters.has_key('component'):
888                        tkt['component'] = self.parameters['component']
889
890                if not msg['Subject']:
891                        tkt['summary'] = u'(No subject)'
892                else:
893                        tkt['summary'] = subject
894
895
896                if set_fields:
897                        rest, keywords = string.split(set_fields, '?')
898
899                        if keywords:
900                                update_fields = self.str_to_dict(keywords)
901                                self.update_ticket_fields(tkt, update_fields)
902
903                # produce e-mail like header
904                #
905                head = ''
906                if self.EMAIL_HEADER > 0:
907                        head = self.email_header_txt(msg)
908
909                message_parts = self.get_message_parts(msg)
910
911                # Must we update some ticket fields properties via body_text
912                #
913                if self.properties:
914                                self.update_ticket_fields(tkt, self.properties)
915
916                if self.DEBUG:
917                        print 'TD: self.get_message_parts ',
918                        print message_parts
919
920                message_parts = self.unique_attachment_names(message_parts)
921                if self.DEBUG:
922                        print 'TD: self.unique_attachment_names',
923                        print message_parts
924               
925                if self.EMAIL_HEADER > 0:
926                        message_parts.insert(0, self.email_header_txt(msg))
927                       
928                body_text = self.body_text(message_parts)
929
930                tkt['description'] = body_text
931
932                #when = int(time.time())
933                #
934                utc = UTC()
935                when = datetime.now(utc)
936
937                if not self.DRY_RUN:
938                        self.id = tkt.insert()
939       
940                changed = False
941                comment = ''
942
943                # some routines in trac are dependend on ticket id     
944                # like alternate notify template
945                #
946                if self.notify_template:
947                        tkt['id'] = self.id
948                        changed = True
949
950                ## Rewrite the description if we have mailto enabled
951                #
952                if self.MAILTO:
953                        changed = True
954                        comment = u'\nadded mailto line\n'
955                        mailto = self.html_mailto_link( m['Subject'])
956
957                        tkt['description'] = u'%s\r\n%s%s\r\n' \
958                                %(head, mailto, body_text)
959       
960                ## Save the attachments to the ticket   
961                #
962                error_with_attachments =  self.attach_attachments(message_parts)
963
964                if error_with_attachments:
965                        changed = True
966                        comment = '%s\n%s\n' %(comment, error_with_attachments)
967
968                if changed:
969                        if self.DRY_RUN:
970                                print 'DRY_RUN: tkt.save_changes(self.author, comment) ', self.author
971                        else:
972                                tkt.save_changes(self.author, comment)
973                                #print tkt.get_changelog(self.db, when)
974
975                if self.notification and not spam:
976                        self.notify(tkt, True)
977
978
979        def attach_attachments(self, message_parts, update=False):
980                '''
981                save any attachments as files in the ticket's directory
982                '''
983                if self.VERBOSE:
984                        print "VB: attach_attachments()"
985
986                if self.DRY_RUN:
987                        print "DRY_RUN: no attachments attached to tickets"
988                        return ''
989
990                count = 0
991
992                # Get Maxium attachment size
993                #
994                max_size = int(self.get_config('attachment', 'max_size'))
995                status   = None
996               
997                for item in message_parts:
998                        # Skip body parts
999                        if not isinstance(item, tuple):
1000                                continue
1001                               
1002                        (original, filename, part) = item
1003                        #
1004                        # Must be tuneables HvB
1005                        #
1006                        path, fd =  util.create_unique_file(os.path.join(self.TMPDIR, filename))
1007                        text = part.get_payload(decode=1)
1008                        if not text:
1009                                text = '(None)'
1010                        fd.write(text)
1011                        fd.close()
1012
1013                        # get the file_size
1014                        #
1015                        stats = os.lstat(path)
1016                        file_size = stats[stat.ST_SIZE]
1017
1018                        # Check if the attachment size is allowed
1019                        #
1020                        if (max_size != -1) and (file_size > max_size):
1021                                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1022                                        %(status, original, file_size, max_size)
1023
1024                                os.unlink(path)
1025                                continue
1026                        else:
1027                                count = count + 1
1028                                       
1029                        # Insert the attachment
1030                        #
1031                        fd = open(path, 'rb')
1032                        att = attachment.Attachment(self.env, 'ticket', self.id)
1033
1034                        # This will break the ticket_update system, the body_text is vaporized
1035                        # ;-(
1036                        #
1037                        if not update:
1038                                att.author = self.author
1039                                att.description = self.email_to_unicode('Added by email2trac')
1040
1041                        att.insert(filename, fd, file_size)
1042
1043                        #except  util.TracError, detail:
1044                        #       print detail
1045
1046                        # Remove the created temporary filename
1047                        #
1048                        fd.close()
1049                        os.unlink(path)
1050
1051                ## return error
1052                #
1053                return status
1054
1055########## Fullblog functions  ###########################################################
1056
1057        def blog(self, id):
1058                """
1059                The blog create/update function
1060                """
1061                # import the modules
1062                #
1063                from tracfullblog.core import FullBlogCore
1064                from tracfullblog.model import BlogPost, BlogComment
1065                from trac.test import Mock, MockPerm
1066
1067                # instantiate blog core
1068                blog = FullBlogCore(self.env)
1069                req = Mock(authname='anonymous', perm=MockPerm(), args={})
1070
1071                if id:
1072
1073                        # update blog
1074                        #
1075                        comment = BlogComment(self.env, id)
1076                        comment.author = self.author
1077
1078                        message_parts = self.get_message_parts(m)
1079                        comment.comment = self.body_text(message_parts)
1080
1081                        blog.create_comment(req, comment)
1082
1083                else:
1084                        # create blog
1085                        #
1086                        import time
1087                        post = BlogPost(self.env, 'blog_'+time.strftime("%Y%m%d%H%M%S", time.gmtime()))
1088
1089                        #post = BlogPost(self.env, blog._get_default_postname(self.env))
1090                       
1091                        post.author = self.author
1092                        post.title = self.email_to_unicode(m['Subject'])
1093
1094                        message_parts = self.get_message_parts(m)
1095                        post.body = self.body_text(message_parts)
1096                       
1097                        blog.create_post(req, post, self.author, u'Created by email2trac', False)
1098
1099
1100
1101########## MAIN function  ###########################################################
1102
1103        def parse(self, fp):
1104                global m
1105
1106                m = email.message_from_file(fp)
1107               
1108                if not m:
1109                        if self.DEBUG:
1110                                print "TD: This is not a valid email message format"
1111                        return
1112                       
1113                # Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1114                try:
1115                        m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1116                except AttributeError, detail:
1117                        pass
1118
1119                if self.DEBUG > 1:        # save the entire e-mail message text
1120                        self.save_email_for_debug(m, True)
1121
1122                self.db = self.env.get_db_cnx()
1123                self.get_sender_info(m)
1124
1125                if not self.email_header_acl('white_list', self.email_addr, True):
1126                        if self.DEBUG > 1 :
1127                                print 'Message rejected : %s not in white list' %(self.email_addr)
1128                        return False
1129
1130                if self.email_header_acl('black_list', self.email_addr, False):
1131                        if self.DEBUG > 1 :
1132                                print 'Message rejected : %s in black list' %(self.email_addr)
1133                        return False
1134
1135                if not self.email_header_acl('recipient_list', self.to_email_addr, True):
1136                        if self.DEBUG > 1 :
1137                                print 'Message rejected : %s not in recipient list' %(self.to_email_addr)
1138                        return False
1139
1140                # If drop the message
1141                #
1142                if self.spam(m) == 'drop':
1143                        return False
1144
1145                elif self.spam(m) == 'spam':
1146                        spam_msg = True
1147                else:
1148                        spam_msg = False
1149
1150                if self.get_config('notification', 'smtp_enabled') in ['true']:
1151                        self.notification = 1
1152                else:
1153                        self.notification = 0
1154
1155
1156                # Check if  FullBlogPlugin is installed
1157                #
1158                blog_enabled = None
1159                if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1160                        blog_enabled = True
1161
1162                if not m['Subject']:
1163                        subject  = 'No Subject'
1164                else:
1165                        subject  = self.email_to_unicode(m['Subject'])         
1166
1167                #
1168                # [hic] #1529: Re: LRZ
1169                # [hic] #1529?owner=bas,priority=medium: Re: LRZ
1170                #
1171                TICKET_RE = re.compile(r"""
1172                        (?P<blog>blog:(?P<blog_id>\w*))
1173                        |(?P<new_fields>[#][?].*)
1174                        |(?P<reply>[#][\d]+:)
1175                        |(?P<reply_fields>[#][\d]+\?.*?:)
1176                        """, re.VERBOSE)
1177
1178                # Find out if this is a ticket or a blog
1179                #
1180                result =  TICKET_RE.search(subject)
1181
1182                if result:
1183                        if result.group('blog'):
1184                                if blog_enabled:
1185                                        self.blog(result.group('blog_id'))
1186                                else:
1187                                        if self.DEBUG:
1188                                                print "Fullblog plugin is not installed"
1189                                                return
1190
1191                        # update ticket + fields
1192                        #
1193                        if result.group('reply_fields') and self.TICKET_UPDATE:
1194                                self.ticket_update(m, result.group('reply_fields'), spam_msg)
1195
1196                        # Update ticket
1197                        #
1198                        elif result.group('reply') and self.TICKET_UPDATE:
1199                                self.ticket_update(m, result.group('reply'), spam_msg)
1200
1201                        # New ticket + fields
1202                        #
1203                        elif result.group('new_fields'):
1204                                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1205
1206                # Create ticket
1207                #
1208                else:
1209                        self.new_ticket(m, subject, spam_msg)
1210
1211########## BODY TEXT functions  ###########################################################
1212
1213        def strip_signature(self, text):
1214                """
1215                Strip signature from message, inspired by Mailman software
1216                """
1217                body = []
1218                for line in text.splitlines():
1219                        if line == '-- ':
1220                                break
1221                        body.append(line)
1222
1223                return ('\n'.join(body))
1224
1225        def reflow(self, text, delsp = 0):
1226                """
1227                Reflow the message based on the format="flowed" specification (RFC 3676)
1228                """
1229                flowedlines = []
1230                quotelevel = 0
1231                prevflowed = 0
1232
1233                for line in text.splitlines():
1234                        from re import match
1235                       
1236                        # Figure out the quote level and the content of the current line
1237                        m = match('(>*)( ?)(.*)', line)
1238                        linequotelevel = len(m.group(1))
1239                        line = m.group(3)
1240
1241                        # Determine whether this line is flowed
1242                        if line and line != '-- ' and line[-1] == ' ':
1243                                flowed = 1
1244                        else:
1245                                flowed = 0
1246
1247                        if flowed and delsp and line and line[-1] == ' ':
1248                                line = line[:-1]
1249
1250                        # If the previous line is flowed, append this line to it
1251                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1252                                flowedlines[-1] += line
1253                        # Otherwise, start a new line
1254                        else:
1255                                flowedlines.append('>' * linequotelevel + line)
1256
1257                        prevflowed = flowed
1258                       
1259
1260                return '\n'.join(flowedlines)
1261
1262        def strip_quotes(self, text):
1263                """
1264                Strip quotes from message by Nicolas Mendoza
1265                """
1266                body = []
1267                for line in text.splitlines():
1268                        if line.startswith(self.EMAIL_QUOTE):
1269                                continue
1270                        body.append(line)
1271
1272                return ('\n'.join(body))
1273
1274        def inline_properties(self, text):
1275                """
1276                Parse text if we use inline keywords to set ticket fields
1277                """
1278                if self.DEBUG:
1279                        print 'TD: inline_properties function'
1280
1281                properties = dict()
1282                body = list()
1283
1284                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1285
1286                for line in text.splitlines():
1287                        match = INLINE_EXP.match(line)
1288                        if match:
1289                                keyword, value = match.groups()
1290                                self.properties[keyword] = value.strip()
1291                                if self.DEBUG:
1292                                        print "TD: inline properties: %s : %s" %(keyword,value)
1293                        else:
1294                                body.append(line)
1295                               
1296                return '\n'.join(body)
1297
1298
1299        def wrap_text(self, text, replace_whitespace = False):
1300                """
1301                Will break a lines longer then given length into several small
1302                lines of size given length
1303                """
1304                import textwrap
1305
1306                LINESEPARATOR = '\n'
1307                reformat = ''
1308
1309                for s in text.split(LINESEPARATOR):
1310                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1311                        if tmp:
1312                                reformat = '%s\n%s' %(reformat,tmp)
1313                        else:
1314                                reformat = '%s\n' %reformat
1315
1316                return reformat
1317
1318                # Python2.4 and higher
1319                #
1320                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1321                #
1322
1323########## EMAIL attachements functions ###########################################################
1324
1325        def inline_part(self, part):
1326                """
1327                """
1328                if self.VERBOSE:
1329                        print "VB: inline_part()"
1330
1331                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1332
1333        def get_message_parts(self, msg):
1334                """
1335                parses the email message and returns a list of body parts and attachments
1336                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1337                """
1338                if self.VERBOSE:
1339                        print "VB: get_message_parts()"
1340
1341                message_parts = list()
1342       
1343                ALTERNATIVE_MULTIPART = False
1344
1345                for part in msg.walk():
1346                        if self.DEBUG:
1347                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
1348                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
1349
1350                        ## Check content type
1351                        #
1352                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
1353
1354                                if self.DEBUG:
1355                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
1356
1357                                continue
1358
1359                        ## Catch some mulitpart execptions
1360                        #
1361                        if part.get_content_type() == 'multipart/alternative':
1362                                ALTERNATIVE_MULTIPART = True
1363                                continue
1364
1365                        ## Skip multipart containers
1366                        #
1367                        if part.get_content_maintype() == 'multipart':
1368                                if self.DEBUG:
1369                                        print "TD: Skipping multipart container"
1370                                continue
1371                       
1372                        ## 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"
1373                        #
1374                        inline = self.inline_part(part)
1375
1376                        ## Drop HTML message
1377                        #
1378                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1379                                if part.get_content_type() == 'text/html':
1380                                        if self.DEBUG:
1381                                                print "TD: Skipping alternative HTML message"
1382
1383                                        ALTERNATIVE_MULTIPART = False
1384                                        continue
1385
1386                        ## Inline text parts are where the body is
1387                        #
1388                        if part.get_content_type() == 'text/plain' and inline:
1389                                if self.DEBUG:
1390                                        print 'TD:               Inline body part'
1391
1392                                # Try to decode, if fails then do not decode
1393                                #
1394                                body_text = part.get_payload(decode=1)
1395                                if not body_text:                       
1396                                        body_text = part.get_payload(decode=0)
1397
1398                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1399                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
1400
1401                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1402                                        body_text = self.reflow(body_text, delsp == 'yes')
1403       
1404                                if self.STRIP_SIGNATURE:
1405                                        body_text = self.strip_signature(body_text)
1406
1407                                if self.STRIP_QUOTES:
1408                                        body_text = self.strip_quotes(body_text)
1409
1410                                if self.INLINE_PROPERTIES:
1411                                        body_text = self.inline_properties(body_text)
1412
1413                                if self.USE_TEXTWRAP:
1414                                        body_text = self.wrap_text(body_text)
1415
1416                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
1417                                #
1418                                charset = part.get_content_charset()
1419                                if not charset:
1420                                        charset = 'iso-8859-15'
1421
1422                                try:
1423                                        ubody_text = unicode(body_text, charset)
1424
1425                                except UnicodeError, detail:
1426                                        ubody_text = unicode(body_text, 'iso-8859-15')
1427
1428                                except LookupError, detail:
1429                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
1430
1431                                if self.VERBATIM_FORMAT:
1432                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1433                                else:
1434                                        message_parts.append('%s' %ubody_text)
1435                        else:
1436                                if self.DEBUG:
1437                                        try:
1438                                                print 'TD:               Filename: %s' % part.get_filename()
1439                                        except UnicodeEncodeError, detail:
1440                                                print 'TD:               Filename: Can not be printed due to non-ascii characters'
1441
1442                                ## Convert 7-bit filename to 8 bits value
1443                                #
1444                                filename = self.email_to_unicode(part.get_filename())
1445                                message_parts.append((filename, part))
1446
1447                return message_parts
1448               
1449        def unique_attachment_names(self, message_parts):
1450                """
1451                """
1452                renamed_parts = []
1453                attachment_names = set()
1454
1455                for item in message_parts:
1456                       
1457                        ## If not an attachment, leave it alone
1458                        #
1459                        if not isinstance(item, tuple):
1460                                renamed_parts.append(item)
1461                                continue
1462                               
1463                        (filename, part) = item
1464
1465                        ## If no filename, use a default one
1466                        #
1467                        if not filename:
1468                                filename = 'untitled-part'
1469
1470                                # Guess the extension from the content type, use non strict mode
1471                                # some additional non-standard but commonly used MIME types
1472                                # are also recognized
1473                                #
1474                                ext = mimetypes.guess_extension(part.get_content_type(), False)
1475                                if not ext:
1476                                        ext = '.bin'
1477
1478                                filename = '%s%s' % (filename, ext)
1479
1480# We now use the attachment insert function
1481#
1482                        ## Discard relative paths in attachment names
1483                        #
1484                        #filename = filename.replace('\\', '/').replace(':', '/')
1485                        #filename = os.path.basename(filename)
1486                        #
1487                        # We try to normalize the filename to utf-8 NFC if we can.
1488                        # Files uploaded from OS X might be in NFD.
1489                        # Check python version and then try it
1490                        #
1491                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1492                        #       try:
1493                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1494                        #       except TypeError:
1495                        #               pass
1496
1497                        # Make the filename unique for this ticket
1498                        num = 0
1499                        unique_filename = filename
1500                        dummy_filename, ext = os.path.splitext(filename)
1501
1502                        while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
1503                                num += 1
1504                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
1505                               
1506                        if self.DEBUG:
1507                                try:
1508                                        print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1509                                except UnicodeEncodeError, detail:
1510                                        print 'Filename can not be printed due to non-ascii characters'
1511
1512                        attachment_names.add(unique_filename)
1513
1514                        renamed_parts.append((filename, unique_filename, part))
1515       
1516                return renamed_parts
1517                       
1518                       
1519        def attachment_exists(self, filename):
1520
1521                if self.DEBUG:
1522                        s = 'TD: attachment already exists: Ticket id : '
1523                        try:
1524                                print "%s%s, Filename : %s" %(s, self.id, filename)
1525                        except UnicodeEncodeError, detail:
1526                                print "%s%s, Filename : Can not be printed due to non-ascii characters" %(s, self.id)
1527
1528                # We have no valid ticket id
1529                #
1530                if not self.id:
1531                        return False
1532
1533                try:
1534                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
1535                        return True
1536                except attachment.ResourceNotFound:
1537                        return False
1538
1539########## TRAC Ticket Text ###########################################################
1540                       
1541        def body_text(self, message_parts):
1542                body_text = []
1543               
1544                for part in message_parts:
1545                        # Plain text part, append it
1546                        if not isinstance(part, tuple):
1547                                body_text.extend(part.strip().splitlines())
1548                                body_text.append("")
1549                                continue
1550                               
1551                        (original, filename, part) = part
1552                        inline = self.inline_part(part)
1553                       
1554                        if part.get_content_maintype() == 'image' and inline:
1555                                body_text.append('[[Image(%s)]]' % filename)
1556                                body_text.append("")
1557                        else:
1558                                body_text.append('[attachment:"%s"]' % filename)
1559                                body_text.append("")
1560                               
1561                body_text = '\r\n'.join(body_text)
1562                return body_text
1563
1564        def html_mailto_link(self, subject):
1565                """
1566                This function returns a HTML mailto tag with the ticket id and author email address
1567                """
1568                if not self.author:
1569                        author = self.email_addr
1570                else:   
1571                        author = self.author
1572
1573                # use urllib to escape the chars
1574                #
1575                s = 'mailto:%s?Subject=%s&Cc=%s' %(
1576                       urllib.quote(self.email_addr),
1577                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
1578                           urllib.quote(self.MAILTO_CC)
1579                           )
1580
1581                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)
1582                return s
1583
1584########## TRAC notify section ###########################################################
1585
1586        def notify(self, tkt, new=True, modtime=0):
1587                """
1588                A wrapper for the TRAC notify function. So we can use templates
1589                """
1590                if self.DRY_RUN:
1591                                print 'DRY_RUN: self.notify(tkt, True) ', self.author
1592                                return
1593                try:
1594                        # create false {abs_}href properties, to trick Notify()
1595                        #
1596                        if not self.VERSION == 0.11:
1597                                self.env.abs_href = Href(self.get_config('project', 'url'))
1598                                self.env.href = Href(self.get_config('project', 'url'))
1599
1600                        tn = TicketNotifyEmail(self.env)
1601
1602                        if self.notify_template:
1603
1604                                if self.VERSION == 0.11:
1605
1606                                        from trac.web.chrome import Chrome
1607
1608                                        if self.notify_template_update and not new:
1609                                                tn.template_name = self.notify_template_update
1610                                        else:
1611                                                tn.template_name = self.notify_template
1612
1613                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1614                                               
1615                                else:
1616
1617                                        tn.template_name = self.notify_template;
1618
1619                        tn.notify(tkt, new, modtime)
1620
1621                except Exception, e:
1622                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
1623
1624
1625
1626########## Parse Config File  ###########################################################
1627
1628def ReadConfig(file, name):
1629        """
1630        Parse the config file
1631        """
1632        if not os.path.isfile(file):
1633                print 'File %s does not exist' %file
1634                sys.exit(1)
1635
1636        config = trac_config.Configuration(file)
1637
1638        # Use given project name else use defaults
1639        #
1640        if name:
1641                sections = config.sections()
1642                if not name in sections:
1643                        print "Not a valid project name: %s" %name
1644                        print "Valid names: %s" %sections
1645                        sys.exit(1)
1646
1647                project =  dict()
1648                for option, value in  config.options(name):
1649                        project[option] = value
1650
1651        else:
1652                # use some trac internals to get the defaults
1653                #
1654                project = config.parser.defaults()
1655
1656        return project
1657
1658
1659if __name__ == '__main__':
1660        # Default config file
1661        #
1662        configfile = '@email2trac_conf@'
1663        project = ''
1664        component = ''
1665        ticket_prefix = 'default'
1666        dry_run = None
1667        verbose = None
1668
1669        ENABLE_SYSLOG = 0
1670
1671
1672        SHORT_OPT = 'chf:np:t:v'
1673        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
1674
1675        try:
1676                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1677        except getopt.error,detail:
1678                print __doc__
1679                print detail
1680                sys.exit(1)
1681       
1682        project_name = None
1683        for opt,value in opts:
1684                if opt in [ '-h', '--help']:
1685                        print __doc__
1686                        sys.exit(0)
1687                elif opt in ['-c', '--component']:
1688                        component = value
1689                elif opt in ['-f', '--file']:
1690                        configfile = value
1691                elif opt in ['-n', '--dry-run']:
1692                        dry_run = True
1693                elif opt in ['-p', '--project']:
1694                        project_name = value
1695                elif opt in ['-t', '--ticket_prefix']:
1696                        ticket_prefix = value
1697                elif opt in ['-v', '--version']:
1698                        verbose = True
1699       
1700        settings = ReadConfig(configfile, project_name)
1701        if not settings.has_key('project'):
1702                print __doc__
1703                print 'No Trac project is defined in the email2trac config file.'
1704                sys.exit(1)
1705       
1706        if component:
1707                settings['component'] = component
1708
1709        # The default prefix for ticket values in email2trac.conf
1710        #
1711        settings['ticket_prefix'] = ticket_prefix
1712        settings['dry_run'] = dry_run
1713        settings['verbose'] = verbose
1714       
1715        if settings.has_key('trac_version'):
1716                version = settings['trac_version']
1717        else:
1718                version = trac_default_version
1719
1720
1721        #debug HvB
1722        #print settings
1723
1724        try:
1725                if version == '0.9':
1726                        from trac import attachment
1727                        from trac.env import Environment
1728                        from trac.ticket import Ticket
1729                        from trac.web.href import Href
1730                        from trac import util
1731                        from trac.Notify import TicketNotifyEmail
1732                elif version == '0.10':
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                        #
1739                        # return  util.text.to_unicode(str)
1740                        #
1741                        # see http://projects.edgewall.com/trac/changeset/2799
1742                        from trac.ticket.notification import TicketNotifyEmail
1743                        from trac import config as trac_config
1744                elif version == '0.11':
1745                        from trac import attachment
1746                        from trac.env import Environment
1747                        from trac.ticket import Ticket
1748                        from trac.web.href import Href
1749                        from trac import config as trac_config
1750                        from trac import util
1751
1752
1753                        #
1754                        # return  util.text.to_unicode(str)
1755                        #
1756                        # see http://projects.edgewall.com/trac/changeset/2799
1757                        from trac.ticket.notification import TicketNotifyEmail
1758                else:
1759                        print 'TRAC version %s is not supported' %version
1760                        sys.exit(1)
1761                       
1762                if settings.has_key('enable_syslog'):
1763                        if SYSLOG_AVAILABLE:
1764                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1765
1766
1767                # Must be set before environment is created
1768                #
1769                if settings.has_key('python_egg_cache'):
1770                        python_egg_cache = str(settings['python_egg_cache'])
1771                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
1772
1773                env = Environment(settings['project'], create=0)
1774
1775                tktparser = TicketEmailParser(env, settings, float(version))
1776                tktparser.parse(sys.stdin)
1777
1778        # Catch all errors ans log to SYSLOG if we have enabled this
1779        # else stdout
1780        #
1781        except Exception, error:
1782                if ENABLE_SYSLOG:
1783                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1784
1785                        etype, evalue, etb = sys.exc_info()
1786                        for e in traceback.format_exception(etype, evalue, etb):
1787                                syslog.syslog(e)
1788
1789                        syslog.closelog()
1790                else:
1791                        traceback.print_exc()
1792
1793                if m:
1794                        tktparser.save_email_for_debug(m, True)
1795
1796                sys.exit(1)
1797# EOB
Note: See TracBrowser for help on using the repository browser.