source: trunk/email2trac.py.in @ 342

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

moved attach_attachments to TRAC ticket section

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 43.9 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 342 2010-03-24 14:39:41Z 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'], body_text)
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        def strip_signature(self, text):
1212                """
1213                Strip signature from message, inspired by Mailman software
1214                """
1215                body = []
1216                for line in text.splitlines():
1217                        if line == '-- ':
1218                                break
1219                        body.append(line)
1220
1221                return ('\n'.join(body))
1222
1223        def reflow(self, text, delsp = 0):
1224                """
1225                Reflow the message based on the format="flowed" specification (RFC 3676)
1226                """
1227                flowedlines = []
1228                quotelevel = 0
1229                prevflowed = 0
1230
1231                for line in text.splitlines():
1232                        from re import match
1233                       
1234                        # Figure out the quote level and the content of the current line
1235                        m = match('(>*)( ?)(.*)', line)
1236                        linequotelevel = len(m.group(1))
1237                        line = m.group(3)
1238
1239                        # Determine whether this line is flowed
1240                        if line and line != '-- ' and line[-1] == ' ':
1241                                flowed = 1
1242                        else:
1243                                flowed = 0
1244
1245                        if flowed and delsp and line and line[-1] == ' ':
1246                                line = line[:-1]
1247
1248                        # If the previous line is flowed, append this line to it
1249                        if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1250                                flowedlines[-1] += line
1251                        # Otherwise, start a new line
1252                        else:
1253                                flowedlines.append('>' * linequotelevel + line)
1254
1255                        prevflowed = flowed
1256                       
1257
1258                return '\n'.join(flowedlines)
1259
1260        def strip_quotes(self, text):
1261                """
1262                Strip quotes from message by Nicolas Mendoza
1263                """
1264                body = []
1265                for line in text.splitlines():
1266                        if line.startswith(self.EMAIL_QUOTE):
1267                                continue
1268                        body.append(line)
1269
1270                return ('\n'.join(body))
1271
1272        def inline_properties(self, text):
1273                """
1274                Parse text if we use inline keywords to set ticket fields
1275                """
1276                if self.DEBUG:
1277                        print 'TD: inline_properties function'
1278
1279                properties = dict()
1280                body = list()
1281
1282                INLINE_EXP = re.compile('\s*[@]\s*([a-zA-Z]+)\s*:(.*)$')
1283
1284                for line in text.splitlines():
1285                        match = INLINE_EXP.match(line)
1286                        if match:
1287                                keyword, value = match.groups()
1288                                self.properties[keyword] = value.strip()
1289                                if self.DEBUG:
1290                                        print "TD: inline properties: %s : %s" %(keyword,value)
1291                        else:
1292                                body.append(line)
1293                               
1294                return '\n'.join(body)
1295
1296
1297        def wrap_text(self, text, replace_whitespace = False):
1298                """
1299                Will break a lines longer then given length into several small
1300                lines of size given length
1301                """
1302                import textwrap
1303
1304                LINESEPARATOR = '\n'
1305                reformat = ''
1306
1307                for s in text.split(LINESEPARATOR):
1308                        tmp = textwrap.fill(s,self.USE_TEXTWRAP)
1309                        if tmp:
1310                                reformat = '%s\n%s' %(reformat,tmp)
1311                        else:
1312                                reformat = '%s\n' %reformat
1313
1314                return reformat
1315
1316                # Python2.4 and higher
1317                #
1318                #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
1319                #
1320
1321        def inline_part(self, part):
1322                """
1323                """
1324                if self.VERBOSE:
1325                        print "VB: inline_part()"
1326
1327                return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
1328
1329        def get_message_parts(self, msg):
1330                """
1331                parses the email message and returns a list of body parts and attachments
1332                body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
1333                """
1334                if self.VERBOSE:
1335                        print "VB: get_message_parts()"
1336
1337                message_parts = list()
1338       
1339                ALTERNATIVE_MULTIPART = False
1340
1341                for part in msg.walk():
1342                        if self.DEBUG:
1343                                print 'TD: Message part: Main-Type: %s' % part.get_content_maintype()
1344                                print 'TD: Message part: Content-Type: %s' % part.get_content_type()
1345
1346                        ## Check content type
1347                        #
1348                        if part.get_content_type() in self.STRIP_CONTENT_TYPES:
1349
1350                                if self.DEBUG:
1351                                        print "TD: A %s attachment named '%s' was skipped" %(part.get_content_type(), part.get_filename())
1352
1353                                continue
1354
1355                        ## Catch some mulitpart execptions
1356                        #
1357                        if part.get_content_type() == 'multipart/alternative':
1358                                ALTERNATIVE_MULTIPART = True
1359                                continue
1360
1361                        ## Skip multipart containers
1362                        #
1363                        if part.get_content_maintype() == 'multipart':
1364                                if self.DEBUG:
1365                                        print "TD: Skipping multipart container"
1366                                continue
1367                       
1368                        ## 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"
1369                        #
1370                        inline = self.inline_part(part)
1371
1372                        ## Drop HTML message
1373                        #
1374                        if ALTERNATIVE_MULTIPART and self.DROP_ALTERNATIVE_HTML_VERSION:
1375                                if part.get_content_type() == 'text/html':
1376                                        if self.DEBUG:
1377                                                print "TD: Skipping alternative HTML message"
1378
1379                                        ALTERNATIVE_MULTIPART = False
1380                                        continue
1381
1382                        ## Inline text parts are where the body is
1383                        #
1384                        if part.get_content_type() == 'text/plain' and inline:
1385                                if self.DEBUG:
1386                                        print 'TD:               Inline body part'
1387
1388                                # Try to decode, if fails then do not decode
1389                                #
1390                                body_text = part.get_payload(decode=1)
1391                                if not body_text:                       
1392                                        body_text = part.get_payload(decode=0)
1393
1394                                format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
1395                                delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
1396
1397                                if self.REFLOW and not self.VERBATIM_FORMAT and format == 'flowed':
1398                                        body_text = self.reflow(body_text, delsp == 'yes')
1399       
1400                                if self.STRIP_SIGNATURE:
1401                                        body_text = self.strip_signature(body_text)
1402
1403                                if self.STRIP_QUOTES:
1404                                        body_text = self.strip_quotes(body_text)
1405
1406                                if self.INLINE_PROPERTIES:
1407                                        body_text = self.inline_properties(body_text)
1408
1409                                if self.USE_TEXTWRAP:
1410                                        body_text = self.wrap_text(body_text)
1411
1412                                ## Get contents charset (iso-8859-15 if not defined in mail headers)
1413                                #
1414                                charset = part.get_content_charset()
1415                                if not charset:
1416                                        charset = 'iso-8859-15'
1417
1418                                try:
1419                                        ubody_text = unicode(body_text, charset)
1420
1421                                except UnicodeError, detail:
1422                                        ubody_text = unicode(body_text, 'iso-8859-15')
1423
1424                                except LookupError, detail:
1425                                        ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
1426
1427                                if self.VERBATIM_FORMAT:
1428                                        message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
1429                                else:
1430                                        message_parts.append('%s' %ubody_text)
1431                        else:
1432                                if self.DEBUG:
1433                                        try:
1434                                                print 'TD:               Filename: %s' % part.get_filename()
1435                                        except UnicodeEncodeError, detail:
1436                                                print 'TD:               Filename: Can not be printed due to non-ascii characters'
1437
1438                                ## Convert 7-bit filename to 8 bits value
1439                                #
1440                                filename = self.email_to_unicode(part.get_filename())
1441                                message_parts.append((filename, part))
1442
1443                return message_parts
1444               
1445        def unique_attachment_names(self, message_parts):
1446                """
1447                """
1448                renamed_parts = []
1449                attachment_names = set()
1450
1451                for item in message_parts:
1452                       
1453                        ## If not an attachment, leave it alone
1454                        #
1455                        if not isinstance(item, tuple):
1456                                renamed_parts.append(item)
1457                                continue
1458                               
1459                        (filename, part) = item
1460
1461                        ## If no filename, use a default one
1462                        #
1463                        if not filename:
1464                                filename = 'untitled-part'
1465
1466                                # Guess the extension from the content type, use non strict mode
1467                                # some additional non-standard but commonly used MIME types
1468                                # are also recognized
1469                                #
1470                                ext = mimetypes.guess_extension(part.get_content_type(), False)
1471                                if not ext:
1472                                        ext = '.bin'
1473
1474                                filename = '%s%s' % (filename, ext)
1475
1476# We now use the attachment insert function
1477#
1478                        ## Discard relative paths in attachment names
1479                        #
1480                        #filename = filename.replace('\\', '/').replace(':', '/')
1481                        #filename = os.path.basename(filename)
1482                        #
1483                        # We try to normalize the filename to utf-8 NFC if we can.
1484                        # Files uploaded from OS X might be in NFD.
1485                        # Check python version and then try it
1486                        #
1487                        #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
1488                        #       try:
1489                        #               filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
1490                        #       except TypeError:
1491                        #               pass
1492
1493                        # Make the filename unique for this ticket
1494                        num = 0
1495                        unique_filename = filename
1496                        dummy_filename, ext = os.path.splitext(filename)
1497
1498                        while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
1499                                num += 1
1500                                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
1501                               
1502                        if self.DEBUG:
1503                                try:
1504                                        print 'TD: Attachment with filename %s will be saved as %s' % (filename, unique_filename)
1505                                except UnicodeEncodeError, detail:
1506                                        print 'Filename can not be printed due to non-ascii characters'
1507
1508                        attachment_names.add(unique_filename)
1509
1510                        renamed_parts.append((filename, unique_filename, part))
1511       
1512                return renamed_parts
1513                       
1514                       
1515        def attachment_exists(self, filename):
1516
1517                if self.DEBUG:
1518                        s = 'TD: attachment already exists: Ticket id : '
1519                        try:
1520                                print "%s%s, Filename : %s" %(s, self.id, filename)
1521                        except UnicodeEncodeError, detail:
1522                                print "%s%s, Filename : Can not be printed due to non-ascii characters" %(s, self.id)
1523
1524                # We have no valid ticket id
1525                #
1526                if not self.id:
1527                        return False
1528
1529                try:
1530                        att = attachment.Attachment(self.env, 'ticket', self.id, filename)
1531                        return True
1532                except attachment.ResourceNotFound:
1533                        return False
1534                       
1535        def body_text(self, message_parts):
1536                body_text = []
1537               
1538                for part in message_parts:
1539                        # Plain text part, append it
1540                        if not isinstance(part, tuple):
1541                                body_text.extend(part.strip().splitlines())
1542                                body_text.append("")
1543                                continue
1544                               
1545                        (original, filename, part) = part
1546                        inline = self.inline_part(part)
1547                       
1548                        if part.get_content_maintype() == 'image' and inline:
1549                                body_text.append('[[Image(%s)]]' % filename)
1550                                body_text.append("")
1551                        else:
1552                                body_text.append('[attachment:"%s"]' % filename)
1553                                body_text.append("")
1554                               
1555                body_text = '\r\n'.join(body_text)
1556                return body_text
1557
1558        def notify(self, tkt, new=True, modtime=0):
1559                """
1560                A wrapper for the TRAC notify function. So we can use templates
1561                """
1562                if self.DRY_RUN:
1563                                print 'DRY_RUN: self.notify(tkt, True) ', self.author
1564                                return
1565                try:
1566                        # create false {abs_}href properties, to trick Notify()
1567                        #
1568                        if not self.VERSION == 0.11:
1569                                self.env.abs_href = Href(self.get_config('project', 'url'))
1570                                self.env.href = Href(self.get_config('project', 'url'))
1571
1572                        tn = TicketNotifyEmail(self.env)
1573
1574                        if self.notify_template:
1575
1576                                if self.VERSION == 0.11:
1577
1578                                        from trac.web.chrome import Chrome
1579
1580                                        if self.notify_template_update and not new:
1581                                                tn.template_name = self.notify_template_update
1582                                        else:
1583                                                tn.template_name = self.notify_template
1584
1585                                        tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
1586                                               
1587                                else:
1588
1589                                        tn.template_name = self.notify_template;
1590
1591                        tn.notify(tkt, new, modtime)
1592
1593                except Exception, e:
1594                        print 'TD: Failure sending notification on creation of ticket #%s: %s' %(self.id, e)
1595
1596        def html_mailto_link(self, subject, body):
1597                """
1598                This function returns a HTML mailto tag with the ticket id and author email address
1599                """
1600                if not self.author:
1601                        author = self.email_addr
1602                else:   
1603                        author = self.author
1604
1605                # use urllib to escape the chars
1606                #
1607                s = 'mailto:%s?Subject=%s&Cc=%s' %(
1608                       urllib.quote(self.email_addr),
1609                           urllib.quote('Re: #%s: %s' %(self.id, subject)),
1610                           urllib.quote(self.MAILTO_CC)
1611                           )
1612
1613                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)
1614                return s
1615
1616
1617def ReadConfig(file, name):
1618        """
1619        Parse the config file
1620        """
1621        if not os.path.isfile(file):
1622                print 'File %s does not exist' %file
1623                sys.exit(1)
1624
1625        config = trac_config.Configuration(file)
1626
1627        # Use given project name else use defaults
1628        #
1629        if name:
1630                sections = config.sections()
1631                if not name in sections:
1632                        print "Not a valid project name: %s" %name
1633                        print "Valid names: %s" %sections
1634                        sys.exit(1)
1635
1636                project =  dict()
1637                for option, value in  config.options(name):
1638                        project[option] = value
1639
1640        else:
1641                # use some trac internals to get the defaults
1642                #
1643                project = config.parser.defaults()
1644
1645        return project
1646
1647
1648if __name__ == '__main__':
1649        # Default config file
1650        #
1651        configfile = '@email2trac_conf@'
1652        project = ''
1653        component = ''
1654        ticket_prefix = 'default'
1655        dry_run = None
1656        verbose = None
1657
1658        ENABLE_SYSLOG = 0
1659
1660
1661        SHORT_OPT = 'chf:np:t:v'
1662        LONG_OPT  =  ['component=', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'verbose']
1663
1664        try:
1665                opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
1666        except getopt.error,detail:
1667                print __doc__
1668                print detail
1669                sys.exit(1)
1670       
1671        project_name = None
1672        for opt,value in opts:
1673                if opt in [ '-h', '--help']:
1674                        print __doc__
1675                        sys.exit(0)
1676                elif opt in ['-c', '--component']:
1677                        component = value
1678                elif opt in ['-f', '--file']:
1679                        configfile = value
1680                elif opt in ['-n', '--dry-run']:
1681                        dry_run = True
1682                elif opt in ['-p', '--project']:
1683                        project_name = value
1684                elif opt in ['-t', '--ticket_prefix']:
1685                        ticket_prefix = value
1686                elif opt in ['-v', '--version']:
1687                        verbose = True
1688       
1689        settings = ReadConfig(configfile, project_name)
1690        if not settings.has_key('project'):
1691                print __doc__
1692                print 'No Trac project is defined in the email2trac config file.'
1693                sys.exit(1)
1694       
1695        if component:
1696                settings['component'] = component
1697
1698        # The default prefix for ticket values in email2trac.conf
1699        #
1700        settings['ticket_prefix'] = ticket_prefix
1701        settings['dry_run'] = dry_run
1702        settings['verbose'] = verbose
1703       
1704        if settings.has_key('trac_version'):
1705                version = settings['trac_version']
1706        else:
1707                version = trac_default_version
1708
1709
1710        #debug HvB
1711        #print settings
1712
1713        try:
1714                if version == '0.9':
1715                        from trac import attachment
1716                        from trac.env import Environment
1717                        from trac.ticket import Ticket
1718                        from trac.web.href import Href
1719                        from trac import util
1720                        from trac.Notify import TicketNotifyEmail
1721                elif version == '0.10':
1722                        from trac import attachment
1723                        from trac.env import Environment
1724                        from trac.ticket import Ticket
1725                        from trac.web.href import Href
1726                        from trac import util
1727                        #
1728                        # return  util.text.to_unicode(str)
1729                        #
1730                        # see http://projects.edgewall.com/trac/changeset/2799
1731                        from trac.ticket.notification import TicketNotifyEmail
1732                        from trac import config as trac_config
1733                elif version == '0.11':
1734                        from trac import attachment
1735                        from trac.env import Environment
1736                        from trac.ticket import Ticket
1737                        from trac.web.href import Href
1738                        from trac import config as trac_config
1739                        from trac import util
1740
1741
1742                        #
1743                        # return  util.text.to_unicode(str)
1744                        #
1745                        # see http://projects.edgewall.com/trac/changeset/2799
1746                        from trac.ticket.notification import TicketNotifyEmail
1747                else:
1748                        print 'TRAC version %s is not supported' %version
1749                        sys.exit(1)
1750                       
1751                if settings.has_key('enable_syslog'):
1752                        if SYSLOG_AVAILABLE:
1753                                ENABLE_SYSLOG =  float(settings['enable_syslog'])
1754
1755
1756                # Must be set before environment is created
1757                #
1758                if settings.has_key('python_egg_cache'):
1759                        python_egg_cache = str(settings['python_egg_cache'])
1760                        os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
1761
1762                env = Environment(settings['project'], create=0)
1763
1764                tktparser = TicketEmailParser(env, settings, float(version))
1765                tktparser.parse(sys.stdin)
1766
1767        # Catch all errors ans log to SYSLOG if we have enabled this
1768        # else stdout
1769        #
1770        except Exception, error:
1771                if ENABLE_SYSLOG:
1772                        syslog.openlog('email2trac', syslog.LOG_NOWAIT)
1773
1774                        etype, evalue, etb = sys.exc_info()
1775                        for e in traceback.format_exception(etype, evalue, etb):
1776                                syslog.syslog(e)
1777
1778                        syslog.closelog()
1779                else:
1780                        traceback.print_exc()
1781
1782                if m:
1783                        tktparser.save_email_for_debug(m, True)
1784
1785                sys.exit(1)
1786# EOB
Note: See TracBrowser for help on using the repository browser.