source: trunk/email2trac.py.in @ 628

Last change on this file since 628 was 628, checked in by bas, 11 years ago

renamed SARA to SURFsara

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 87.7 KB
Line 
1#!@PYTHON@
2#
3# This file is part of the email2trac utils
4#
5#       Copyright 2002 SURFsara
6#
7#       Licensed under the Apache License, Version 2.0 (the "License");
8#       you may not use this file except in compliance with the License.
9#       You may obtain a copy of the License at
10#
11#       http://www.apache.org/licenses/LICENSE-2.0
12#
13#       Unless required by applicable law or agreed to in writing, software
14#       distributed under the License is distributed on an "AS IS" BASIS,
15#       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16#       See the License for the specific language governing permissions and
17#       limitations under the License.
18#
19# For vi/emacs or other use tabstop=4 (vi: set ts=4)
20#
21"""
22email2trac.py -- Email -> TRAC tickets
23
24A MTA filter to create Trac tickets from inbound emails.
25
26first proof of concept from:
27 Copyright 2005, Daniel Lundin <daniel@edgewall.com>
28 Copyright 2005, Edgewall Software
29
30Authors:
31  Bas van der Vlies <bas.vandervlies@surfsara.nl>
32  Walter de Jong <walter.dejong@surfsara.nl>
33
34How to use
35----------
36 * See https://oss.trac.surfsara.nl/email2trac/
37
38 * Commandline opions:
39                -h,--help
40                -A, --agilo
41                -d, --debug
42                -E, --virtualenv <path>
43                -f,--file  <configuration file>
44                -n,--dry-run
45                -p, --project <project name>
46                -t, --ticket_prefix <name>
47                -v, --verbose
48
49SVN Info:
50        $Id: email2trac.py.in 628 2013-06-18 10:05:30Z bas $
51"""
52import os
53import sys
54import string
55import getopt
56import time
57import email
58import email.Iterators
59import email.Header
60import re
61import urllib
62import unicodedata
63import mimetypes
64import traceback
65import logging
66import logging.handlers
67import UserDict
68import tempfile
69
70from datetime import timedelta, datetime
71from stat import *
72
73## Some global variables
74#
75m = None
76
77# This is to for the function AlwaysNotifyReporter
78sender_email = None
79
80class SaraDict(UserDict.UserDict):
81    def __init__(self, dictin = None):
82        UserDict.UserDict.__init__(self)
83        self.name = None
84       
85        if dictin:
86            if dictin.has_key('name'):
87                self.name = dictin['name']
88                del dictin['name']
89            self.data = dictin
90           
91    def get_value(self, name):
92        if self.has_key(name):
93            return self[name]
94        else:
95            return None
96               
97    def __repr__(self):
98        return repr(self.data)
99
100    def __str__(self):
101        return str(self.data)
102           
103    def __getattr__(self, name):
104        """
105        override the class attribute get method. Return the value
106        from the dictionary
107        """
108        if self.data.has_key(name):
109            return self.data[name]
110        else:
111            return None
112           
113    def __setattr__(self, name, value):
114        """
115        override the class attribute set method only when the UserDict
116        has set its class attribute
117        """
118        if self.__dict__.has_key('data'):
119            self.data[name] = value
120        else:
121            self.__dict__[name] = value
122
123    def __iter__(self):
124        return iter(self.data.keys())
125
126class TicketEmailParser(object):
127    env = None
128    comment = '> '
129
130    def __init__(self, env, parameters, logger, version):
131        self.env = env
132
133        # Database connection
134        #
135        self.db = None
136
137        # Save parameters
138        #
139        self.parameters = parameters
140        self.logger = logger
141
142        # Some useful mail constants
143        #
144        self.email_name = None
145        self.email_addr = None
146        self.email_from = None
147        self.author     = None
148        self.id         = None
149
150        ## Will be set to True if the user has ben registered
151        #
152        self.allow_registered_user = False
153       
154        self.STRIP_CONTENT_TYPES = list()
155
156        ## fields properties via body_text
157        #
158        self.properties = dict()
159
160        self.VERSION = version
161
162        self.get_config = self.env.config.get
163
164        ## init function ##
165        #
166        self.setup_parameters()
167
168    def setup_parameters(self):
169        if self.parameters.umask:
170            os.umask(self.parameters.umask)
171
172        if not self.parameters.spam_level:
173            self.parameters.spam_level = 0
174
175        if not self.parameters.spam_header:
176            self.parameters.spam_header = 'X-Spam-Score'
177
178        if not self.parameters.email_quote:
179            self.parameters.email_quote = '^> .*'
180            self.parameters.skip_line_regex = '^> .*'
181
182        if not self.parameters.ticket_update_by_subject_lookback:
183            self.parameters.ticket_update_by_subject_lookback = 30
184
185        if self.parameters.verbatim_format == None:
186            self.parameters.verbatim_format = 1
187
188        if self.parameters.reflow == None:
189            self.parameters.reflow = 1
190
191        if self.parameters.binhex:
192            self.STRIP_CONTENT_TYPES.append('application/mac-binhex40')
193
194        if self.parameters.applesingle:
195            self.STRIP_CONTENT_TYPES.append('application/applefile')
196
197        if self.parameters.appledouble:
198            self.STRIP_CONTENT_TYPES.append('application/applefile')
199
200        if self.parameters.strip_content_types:
201            items = self.parameters.strip_content_types.split(',')
202            for item in items:
203                self.STRIP_CONTENT_TYPES.append(item.strip())
204
205        if self.parameters.tmpdir:
206            self.parameters.tmpdir = os.path.normcase(str(self.parameters['tmpdir']))
207        else:
208            self.parameters.tmpdir = os.path.normcase('/tmp')
209
210        if self.parameters.email_triggers_workflow == None:
211            self.parameters.email_triggers_workflow = 1
212
213        if not self.parameters.subject_field_separator:
214            self.parameters.subject_field_separator = '&'
215        else:
216            self.parameters.subject_field_separator = self.parameters.subject_field_separator.strip()
217
218        if not self.parameters.strip_signature_regex:
219            self.parameters.strip_signature_regex = '^-- $'
220            self.parameters.cut_line_regex = '^-- $'
221
222        self.trac_smtp_from = self.get_config('notification', 'smtp_from')
223        self.smtp_default_domain = self.get_config('notification', 'smtp_default_domain')
224
225
226        self.system = None
227
228########## Email Header Functions ###########################################################
229
230    def spam(self, message):
231        """
232        # X-Spam-Score: *** (3.255) BAYES_50,DNS_FROM_AHBL_RHSBL,HTML_
233        # Note if Spam_level then '*' are included
234        """
235        self.logger.debug('function spam')
236
237        spam = False
238        if message.has_key(self.parameters.spam_header):
239            spam_l = string.split(message[self.parameters.spam_header])
240            #self.logger.info('Spam header: %s' %(message[self.parameters.spam_header]))
241
242            try:
243                number = spam_l[0].count('*')
244            except IndexError, detail:
245                number = 0
246               
247            if number >= self.parameters.spam_level:
248                spam = True
249               
250        ## treat virus mails as spam
251        #
252        elif message.has_key('X-Virus-found'):         
253            spam = True
254
255        ## Astaro firewall spam handling
256        #
257        elif message.get('X-Spam-Flag') == "YES" and message.get('X-Spam-Result') == "Spam":
258            spam = True
259
260        ## How to handle SPAM messages
261        #
262        if self.parameters.drop_spam and spam:
263
264            self.logger.info('Message is a SPAM. Automatic ticket insertion refused (SPAM level > %d)' %self.parameters.spam_level)
265            return 'drop'   
266
267        elif spam:
268
269            return 'Spam'   
270        else:
271
272            return False
273
274    def email_header_acl(self, keyword, message_field, default):
275        """
276        This function wil check if the email address is allowed or denied
277        to send mail to the ticket list
278        """
279        self.logger.debug('function email_header_acl: %s' %keyword)
280
281        try:
282            mail_addresses = self.parameters[keyword]
283
284            # Check if we have an empty string
285            #
286            if not mail_addresses:
287                return default
288
289        except KeyError, detail:
290            self.logger.debug('\t %s not defined, all messages are allowed.' %(keyword))
291
292            return default
293
294        mail_addresses = string.split(mail_addresses, ',')
295        message_addresses = string.split(message_field, ',')
296
297        for entry in mail_addresses:
298            entry = entry.strip()
299            TO_RE = re.compile(entry, re.VERBOSE|re.IGNORECASE)
300
301            for addr in message_addresses:
302                addr = addr.strip()
303
304                if self.parameters.compare_function_list in [ 'matches', 'match']:
305                    s = '\t%s matches %s' %(addr, entry)
306                    result =  TO_RE.match(addr)
307                else:
308                    s = '\t%s contains %s' %(addr, entry)
309                    result =  TO_RE.search(addr)
310
311                if result:
312                    self.logger.debug(s)
313                    return True
314
315        return False
316
317    def email_header_txt(self, m):
318        """
319        Display To and CC addresses in description field
320        """
321        s = ''
322
323        if m['To'] and len(m['To']) > 0:
324            s = "'''To:''' %s\r\n" %(m['To'])
325        if m['Cc'] and len(m['Cc']) > 0:
326            s = "%s'''Cc:''' %s\r\n" % (s, m['Cc'])
327
328        return  self.email_to_unicode(s)
329
330
331    def get_sender_info(self, message):
332        """
333        Get the default author name and email address from the message
334        """
335        self.logger.debug('function get_sender_info')
336
337        to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
338        self.email_to_addrs = list()
339
340        for n,e in to_addrs:
341            self.email_to_addrs.append(e)
342
343        self.email_to_addrs = ','.join(self.email_to_addrs)
344
345
346        self.email_name, self.email_addr  = email.Utils.parseaddr(message['from'])
347
348        ## decode email name can contain charset
349        #
350        self.email_name = self.email_to_unicode(self.email_name)
351
352        dstr = '\t email name: %s, email address: %s' %(self.email_name, self.email_addr) 
353        self.logger.debug(dstr)
354
355        ## Trac can not handle author's name that contains spaces
356        #
357        if self.email_addr.lower() == self.trac_smtp_from.lower():
358            if self.email_name:
359                self.author = self.email_name
360            else:
361                self.author = "email2trac"
362        else:
363            self.author = self.email_addr
364
365        if self.parameters.ignore_trac_user_settings:
366            return
367
368        # Is this a registered user, use email address as search key:
369        # result:
370        #   u : login name
371        #   n : Name that the user has set in the settings tab
372        #   e : email address that the user has set in the settings tab
373        #
374        users = [ (u,n,e) for (u, n, e) in self.env.get_known_users(self.db)
375            if (
376                (e and (e.lower() == self.email_addr.lower())) or
377                (u.lower() + '@' + self.smtp_default_domain.lower() == self.email_addr.lower())
378            )
379            ]
380
381        if len(users) >= 1:
382            self.email_from = users[0][2]
383            self.author = users[0][0]
384
385            self.logger.debug('\tget_sender_info: found registered user: %s' %(self.author) )
386
387            if self.parameters.white_list_registered_users:
388                self.logger.debug('\tget_sender_info: added registered user to white list')
389                self.allow_registered_user = True
390                   
391
392    def set_cc_fields(self, ticket, message, update=False):
393        """
394        Set all the right fields for a new ticket
395        """
396        self.logger.debug('function set_cc_fields')
397
398
399        # Put all CC-addresses in ticket CC field
400        #
401        if self.parameters.reply_all:
402
403            email_cc = ''
404
405            cc_addrs = email.Utils.getaddresses( message.get_all('cc', []) )
406
407            if not cc_addrs:
408                return
409
410            ## Build a list of forbidden CC addresses
411            #
412            #to_addrs = email.Utils.getaddresses( message.get_all('to', []) )
413            #to_list = list()
414            #for n,e in to_addrs:
415            #   to_list.append(e)
416
417            if update:
418                self.logger.debug("\tupdate cc-fields")
419
420            for name,addr in cc_addrs:
421       
422                ## Prevent mail loop
423                #
424                if addr == self.trac_smtp_from:
425                    self.logger.debug("\tSkipping %s email address for CC-field, same as smtp_from address in trac.ini " %(addr))
426                    continue
427
428                ## Alwyas remove reporter email address
429                #
430                elif addr == self.email_addr:
431                    self.logger.debug("\tSkipping reporter email address for CC-field")
432                    continue
433
434                else:
435                    if email_cc:
436                        email_cc = '%s, %s' %(email_cc, addr)
437                    else:
438                        email_cc = addr
439
440            if email_cc:
441                self.logger.debug('\tCC fields: %s' %email_cc)
442                ticket['cc'] = self.email_to_unicode(email_cc)
443
444
445    def acl_list_from_file(self, f, keyword):
446        """
447        Read the email address from a file
448        """
449        self.logger.debug('function acl_list_from_file %s : %s' %(f, keyword))
450       
451        if  not os.path.isfile(f):
452            self.logger.error('%s_file: %s does not exists' %(keyword, f) )
453        else:
454            ## read whole file and replace '\n' with ''
455            #
456            addr_l = open(f, 'r').readlines()
457            s = ','.join(addr_l).replace('\n','')
458
459            try:
460                self.parameters[keyword] = "%s,%s" %(self.parameters[keyword], s)
461            except KeyError, detail:
462                self.parameters[keyword] = s
463
464########## DEBUG functions  ###########################################################
465
466    def debug_body(self, message_body, temp_file):
467        """
468        """
469        self.logger.debug('function debug_body:')
470
471        body_file = "%s.body" %(temp_file)
472        fx = open(body_file, 'wb')
473
474        if self.parameters.dry_run:
475            self.logger.info('DRY-RUN: not saving body to %s' %(body_file))
476            return
477
478        self.logger.debug('writing body to %s' %(body_file))
479        if not message_body:
480                message_body = '(None)'
481
482        message_body = message_body.encode('utf-8')
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, temp_file):
492        """
493        """
494        self.logger.debug('function debug_attachments')
495       
496        n = 0
497        for item in message_parts:
498
499            ## Skip inline text parts
500            #
501            if not isinstance(item, tuple):
502                continue
503               
504            (original, filename, part) = item
505            self.logger.debug('\t part%d: Content-Type: %s' % (n, part.get_content_type()) )
506
507            dummy_filename, ext = os.path.splitext(filename)
508
509            n = n + 1
510            part_name = 'part%d%s' %(n, ext)
511            part_file = "%s.%s" %(temp_file, part_name)
512
513            s = 'writing %s: filename: %s' %(part_file, filename)
514            self.print_unicode(s)
515
516            ## Forbidden chars, just write part files instead of names
517            #
518            #filename = filename.replace('\\', '_')
519            #filename = filename.replace('/', '_')
520            #filename = filename + '.att_email2trac'
521            # part_file = os.path.join(self.parameters.tmpdir, filename)
522            #part_file = util.text.unicode_quote(part_file)
523            #self.print_unicode(part_file)
524
525            if self.parameters.dry_run:
526                self.logger.info('DRY_RUN: NOT saving attachments')
527                continue
528
529            fx = open(part_file, 'wb')
530            text = part.get_payload(decode=1)
531
532            if not text:
533                text = '(None)'
534
535            fx.write(text)
536            fx.close()
537
538            try:
539                os.chmod(part_file,S_IRWXU|S_IRWXG|S_IRWXO)
540            except OSError:
541                pass
542
543    def save_email_for_debug(self, message, project_name, create_tempfile=False):
544
545        if self.parameters.dry_run:
546            self.logger.debug('DRY_RUN: NOT saving email message')
547            return
548
549        if create_tempfile:
550            (fd, tmp_file) = tempfile.mkstemp('.%s.email2trac' %project_name)
551            fx = os.fdopen(fd, 'wb')
552        else:
553            tmp_file = os.path.join(self.parameters.tmpdir, 'msg.%s.email2trac' %project_name)
554            fx = open(tmp_file, 'wb')
555
556
557        self.logger.debug('saving email to %s' %(tmp_file))
558        fx.write('%s' % message)
559        fx.close()
560       
561        try:
562            os.chmod(tmp_file, S_IRWXU|S_IRWXG|S_IRWXO)
563        except OSError:
564            pass
565
566        message_parts = self.get_message_parts(message)
567        message_parts = self.unique_attachment_names(message_parts)
568        body_text = self.get_body_text(message_parts)
569        self.debug_body(body_text, tmp_file)
570        self.debug_attachments(message_parts, tmp_file)
571
572########## Conversion functions  ###########################################################
573
574    def email_to_unicode(self, message_str):
575        """
576        Email has 7 bit ASCII code, convert it to unicode with the charset
577        that is encoded in 7-bit ASCII code and encode it as utf-8 so Trac
578        understands it.
579        """
580        self.logger.debug("function email_to_unicode")
581
582        results =  email.Header.decode_header(message_str)
583
584        s = None
585        for text,format in results:
586            if format:
587                try:
588                    temp = unicode(text, format)
589                except UnicodeError, detail:
590                    # This always works
591                    #
592                    msg = 'ERROR: Could not find charset: %s, please install' %format
593                    self.logger.error(msg)
594                    temp = unicode(text, 'iso-8859-15')
595                except LookupError, detail:
596                    msg = 'ERROR: Could not find charset: %s, please install' %format
597                    self.logger.error(msg)
598                    #temp = unicode(text, 'iso-8859-15')
599                    temp = message_str
600                       
601            else:
602                temp = string.strip(text)
603                temp = unicode(text, 'iso-8859-15')
604
605            if s:
606                s = '%s %s' %(s, temp)
607            else:
608                s = '%s' %temp
609
610        return s
611
612    def str_to_dict(self, s):
613        """
614        Transfrom a string of the form [<key>=<value>]+ to dict[<key>] = <value>
615        """
616        self.logger.debug("function str_to_dict")
617
618        fields = string.split(s, self.parameters.subject_field_separator)
619
620        result = dict()
621        for field in fields:
622            try:
623                index, value = string.split(field, '=')
624
625                # We can not change the description of a ticket via the subject
626                # line. The description is the body of the email
627                #
628                if index.lower() in ['description']:
629                    continue
630
631                if value:
632                    result[index.lower()] = value
633
634            except ValueError:
635                pass
636        return result
637
638    def print_unicode(self,s):
639        """
640        This function prints unicode strings if possible else it will quote it
641        """
642        try:
643            self.logger.debug(s)
644        except UnicodeEncodeError, detail:
645            self.logger.debug(util.text.unicode_quote(s))
646
647
648    def html_2_txt(self, data):
649        """
650        Various routines to convert html syntax to valid trac wiki syntax
651        """
652        self.logger.debug('function html_2_txt')
653
654        ## This routine make an safe html that can be include
655        #  in trac, but no further text processing can be done
656        #
657#       try:
658#           from lxml.html.clean import Cleaner
659#           tags_rm = list()
660#           tags_rm.append('body')
661#
662#           cleaner = Cleaner(remove_tags=tags_rm )
663#           parsed_data = cleaner.clean_html(data)
664#           parsed_data = '\n{{{\n#!html\n' + parsed_data + '\n}}}\n'
665#
666#           return parsed_data
667#           
668#       except ImportError::
669#           pass
670
671        parsed_data = None
672        if self.parameters.html2text_cmd:
673            (fd, tmp_file) = tempfile.mkstemp('email2trac.html')
674            f = os.fdopen(fd, 'w')
675
676            cmd = '%s %s' %(self.parameters.html2text_cmd, tmp_file)
677            self.logger.debug('\t html2text conversion %s'%(cmd))
678   
679            if self.parameters.dry_run:
680                self.logger.info('DRY_RUN: html2text conversion command: %s\n' %(cmd))
681
682            else:
683                f.write(data)
684                f.close()
685
686                lines = os.popen(cmd).readlines()
687                parsed_data =  ''.join(lines)
688
689                os.unlink(tmp_file)
690
691        else:
692            self.logger.debug('\t No html2text conversion tool specified in email2trac.conf')
693
694        return parsed_data
695
696    def check_filename_length(self, name):
697        """
698        To bypass a bug in Trac
699        check if the filename lenght is not larger then OS limit.
700          yes : return truncated filename
701          no  : return unmodified filename
702        """
703        self.logger.debug('function check_filename_length: ')
704
705        if not name:
706            return 'None'
707       
708        dummy_filename, ext = os.path.splitext(name)
709
710        ## Trac uses this format
711        #
712        try:
713            quote_format = util.text.unicode_quote(dummy_filename)
714
715        except UnicodeDecodeError, detail:
716            ## last resort convert to unicode
717            #
718            dummy_filename = util.text.to_unicode(dummy_filename)
719            quote_format = util.text.unicode_quote(dummy_filename)
720
721        ## Determine max filename lenght
722        #
723        try:
724            filemax_length = os.pathconf('/', 'PC_NAME_MAX')
725        except AttributeError, detail:
726            filemax_lenght = 240
727
728        if len(quote_format) <= filemax_length:
729            return name
730
731        else:
732            ## Truncate file to filemax_length and reserve room for extension
733            #  We must break on a boundry
734            #
735            length = filemax_length - 6
736
737            for i in range(0,10):
738
739                truncated = quote_format[ : (length - i)]
740
741                try:
742                    unqoute_truncated = util.text.unicode_unquote(truncated)
743                    unqoute_truncated = unqoute_truncated + ext
744
745                    self.print_unicode('\t ' + unqoute_truncated)
746
747                    break
748
749                except UnicodeDecodeError, detail:
750                    continue
751
752
753            return unqoute_truncated
754
755########## TRAC ticket functions  ###########################################################
756
757    def mail_workflow(self, tkt):
758        """
759        """
760        self.logger.debug('function mail_workflow: ')
761       
762        req = Mock(authname=self.author, perm=MockPerm(), args={})
763        ticket_system = TicketSystem(self.env)
764       
765        try:
766            workflow = self.parameters['workflow_%s' %tkt['status'].lower()]
767
768        except KeyError:
769            ## fallback for compability (Will be deprecated)
770            #  workflow can be none.
771            #
772            workflow = None
773            if tkt['status'] in ['closed']:
774                workflow = self.parameters.workflow
775
776        if workflow:
777
778            ## process all workflow implementations
779            #
780            tkt_module = TicketModule(self.env)
781            field_changes, problems = tkt_module.get_ticket_changes(req, tkt, workflow)
782
783            for field in field_changes.keys():
784
785                ## We have already processed these fields
786                #
787                if not field in ['summary', 'description']:
788                    s = 'workflow : %s, field %s : %s, by %s' \
789                       %(workflow, field, field_changes[field]['new'],field_changes[field]['by'] )
790                    self.logger.debug(s)
791
792                    tkt[field] = field_changes[field]['new']
793
794            return True
795
796        else:
797            return False
798
799    def check_permission_participants(self, tkt, action):
800        """
801        Check if the mailer is allowed to update the ticket
802        """
803        self.logger.debug('function check_permission_participants %s')
804
805        if tkt['reporter'].lower() in [self.author.lower(), self.email_addr.lower()]:
806            self.logger.debug('ALLOW, %s is the ticket reporter' %(self.email_addr))
807
808            return True
809
810        perm = PermissionSystem(self.env)
811        if perm.check_permission(action, self.author):
812            self.logger.debug('ALLOW, %s has trac permission to update the ticket' %(self.author))
813
814            return True
815       
816        # Is the updater in the CC?
817        try:
818            cc_list = tkt['cc'].split(',')
819            for cc in cc_list:
820                if self.email_addr.lower() in cc.lower.strip():
821                    self.logger.debug('ALLOW, %s is in the CC' %(self.email_addr))
822
823                    return True
824
825        except KeyError:
826            pass
827
828        return False
829
830    def check_permission(self, tkt, action):
831        """
832        check if the reporter has the right permission for the action:
833          - TICKET_CREATE
834          - TICKET_MODIFY
835          - TICKET_APPEND
836          - TICKET_CHGPROP
837
838        There are three models:
839            - None      : no checking at all
840            - trac      : check the permission via trac permission model
841            - email2trac: ....
842        """
843        self.logger.debug("function check_permission: %s" %(action))
844
845        if self.parameters.ticket_permission_system in ['trac']:
846
847            perm = PermissionCache(self.env, self.author)
848            if perm.has_permission(action):
849                return True
850            else:
851                return False
852
853        elif self.parameters.ticket_permission_system in ['update_restricted_to_participants']:
854            return (self.check_permission_participants(tkt, action))   
855
856        ## Default is to allow everybody ticket updates and ticket creation
857        #
858        else:
859                return True
860
861
862    def update_ticket_fields(self, ticket, user_dict, new=None):
863        """
864        This will update the ticket fields. It will check if the
865        given fields are known and if the right values are specified
866        It will only update the ticket field value:
867            - If the field is known
868            - If the value supplied is valid for the ticket field.
869              If not then there are two options:
870               1) Skip the value (new=None)
871               2) Set default value for field (new=1)
872        """
873        self.logger.debug("function update_ticket_fields")
874
875        ## Check only permission model on ticket updates
876        #
877        if not new:
878            if self.parameters.ticket_permission_system:
879                if not self.check_permission(ticket, 'TICKET_CHGPROP'):
880                    self.logger.info('Reporter: %s has no permission to change ticket properties' %self.author)
881                    return False
882
883        ## Build a system dictionary from the ticket fields
884        #  with field as index and option as value
885        #
886        sys_dict = dict()
887
888        for field in ticket.fields:
889
890            try:
891                sys_dict[field['name']] = field['options']
892
893            except KeyError:
894                sys_dict[field['name']] = None
895                pass
896
897        ## Check user supplied fields an compare them with the
898        #  system one's
899        #
900        for field,value in user_dict.items():
901       
902            s = 'user_field\t %s = %s' %(field,value)
903            self.print_unicode(s)
904
905            if not field in sys_dict.keys(): 
906                self.logger.debug('%s is not a valid field for tickets' %(field))
907                continue
908
909            ## To prevent mail loop
910            #
911            if field == 'cc':
912
913                cc_list = user_dict['cc'].split(',')
914
915                if self.trac_smtp_from in cc_list:
916                    self.logger.debug('MAIL LOOP: %s is not allowed as CC address' %(self.trac_smtp_from))
917
918                    cc_list.remove(self.trac_smtp_from)
919
920                value = ','.join(cc_list)
921               
922
923            ## Check if every value is allowed for this field
924            #
925            if sys_dict[field]:
926
927                if value in sys_dict[field]:
928                    ticket[field] = value
929                else:
930                    ## Must we set a default if value is not allowed
931                    #
932                    if new:
933                        value = self.get_config('ticket', 'default_%s' %(field) )
934
935            else:
936
937                ticket[field] = value
938
939            s = 'ticket_field\t %s = %s' %(field,  ticket[field])
940            self.print_unicode(s)
941
942    def ticket_update(self, m, id, spam):
943        """
944        If the current email is a reply to an existing ticket, this function
945        will append the contents of this email to that ticket, instead of
946        creating a new one.
947        """
948        self.logger.debug("function ticket_update")
949
950        if not self.parameters.ticket_update:
951            self.logger.debug("ticket_update disabled")
952            return False
953
954        ## Must we update ticket fields
955        #
956        update_fields = dict()
957        try:
958            id, keywords = string.split(id, '?')
959
960            update_fields = self.str_to_dict(keywords)
961
962            ## Strip '#'
963            #
964            self.id = int(id[1:])
965
966        except ValueError:
967
968            ## Strip '#'
969            #
970            self.id = int(id[1:])
971
972        self.logger.debug("\tticket id: %s" %id)
973
974        ## When is the change committed
975        #
976        if self.VERSION < 0.11:
977            when = int(time.time())
978        else:
979            when = datetime.now(util.datefmt.utc)
980
981        try:
982            tkt = Ticket(self.env, self.id, self.db)
983
984        except util.TracError, detail:
985
986            ## Not a valid ticket
987            #
988            self.logger.info("\tCreating a new ticket, ticket id: %s does not exists" %id)
989            self.id = None
990            return False
991
992        ## Check the permission of the reporter
993        #
994        if self.parameters.ticket_permission_system:
995            if not self.check_permission(tkt, 'TICKET_APPEND'):
996                self.logger.info('Reporter: %s has no permission to add comments or attachments to tickets' %self.author)
997                return False
998
999        ## How many changes has this ticket
1000        #
1001        grouped = TicketModule(self.env).grouped_changelog_entries(tkt, self.db)
1002        cnum = sum(1 for e in grouped) + 1
1003
1004
1005        ## reopen the ticket if it is was closed
1006        #  We must use the ticket workflow framework
1007        #
1008        if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
1009
1010            if not self.mail_workflow(tkt):
1011
1012                if tkt['status'] in ['closed']:
1013                    tkt['status'] = 'reopened'
1014                    tkt['resolution'] = ''
1015
1016        ## Old pre 0.11 situation
1017        #
1018        elif self.parameters.email_triggers_workflow:
1019
1020            self.logger.debug('\temail triggers workflow pre trac 0.11')
1021
1022            if tkt['status'] in ['closed']:
1023                tkt['status'] = 'reopened'
1024                tkt['resolution'] = ''
1025
1026        else:
1027            self.logger.debug('\temail triggers workflow disabled')
1028
1029        ## Must we update some ticket fields properties via subject line
1030        #
1031        if update_fields:
1032            self.update_ticket_fields(tkt, update_fields)
1033
1034
1035        message_parts = self.get_message_parts(m)
1036        message_parts = self.unique_attachment_names(message_parts)
1037
1038        ## Must we update some ticket fields properties via inline comments
1039        # in body_text
1040        #
1041        if self.properties:
1042                self.update_ticket_fields(tkt, self.properties)
1043
1044        ## Must we update the CC ticket field
1045        #
1046        self.set_cc_fields(tkt, m, update=True)
1047
1048        if self.parameters.email_header:
1049            message_parts.insert(0, self.email_header_txt(m))
1050
1051        body_text = self.get_body_text(message_parts)
1052
1053        error_with_attachments = self.attach_attachments(message_parts)
1054
1055        if body_text.strip() or update_fields or self.properties:
1056
1057            if self.parameters.dry_run:
1058                s = 'DRY_RUN: tkt.save_changes(self.author, body_text, ticket_change_number) %s %s' %(self.author, cnum)
1059                self.logger.info(s)
1060
1061            else:
1062                if error_with_attachments:
1063                    body_text = '%s\\%s' %(error_with_attachments, body_text)
1064
1065                self.logger.debug('\ttkt.save_changes(%s, %d)' %(self.author, cnum))
1066                tkt.save_changes(self.author, body_text, when, None, str(cnum))
1067           
1068        if not spam:
1069            self.notify(tkt, False, when)
1070
1071        return True
1072
1073    def set_ticket_fields(self, ticket):
1074        """
1075        set the ticket fields to value specified
1076            - /etc/email2trac.conf with <prefix>_<field>
1077            - trac default values, trac.ini
1078        """
1079        self.logger.debug('function set_ticket_fields')
1080
1081        user_dict = dict()
1082
1083        for field in ticket.fields:
1084
1085            name = field['name']
1086
1087            ## default trac value
1088            #
1089            if not field.get('custom'):
1090                value = self.get_config('ticket', 'default_%s' %(name) )
1091
1092                ## skip this field can only be set by email2trac.conf
1093                #
1094                if name in ['resolution']:
1095                    value = None
1096
1097            else:
1098                ##  Else get the default value for reporter
1099                #
1100                value = field.get('value')
1101                options = field.get('options')
1102
1103                if value and options and (value not in options):
1104                     value = options[int(value)]
1105   
1106            s = 'trac[%s] = %s' %(name, value)
1107            self.print_unicode(s)
1108
1109            ## email2trac.conf settings
1110            #
1111            prefix = self.parameters.ticket_prefix
1112            try:
1113                value = self.parameters['%s_%s' %(prefix, name)]
1114
1115                s = 'email2trac[%s] = %s ' %(name, value)
1116                self.print_unicode(s)
1117
1118            except KeyError, detail:
1119                pass
1120       
1121            if value:
1122                user_dict[name] = value
1123
1124                s = 'used %s = %s' %(name, value)
1125                self.print_unicode(s)
1126
1127        self.update_ticket_fields(ticket, user_dict, new=1)
1128
1129        if 'status' not in user_dict.keys():
1130            ticket['status'] = 'new'
1131
1132
1133    def ticket_update_by_subject(self, subject):
1134        """
1135        This list of Re: prefixes is probably incomplete. Taken from
1136        wikipedia. Here is how the subject is matched
1137          - Re: <subject>
1138          - Re: (<Mail list label>:)+ <subject>
1139
1140        So we must have the last column
1141        """
1142        self.logger.debug('function ticket_update_by_subject')
1143
1144        found_id = None
1145        if self.parameters.ticket_update and self.parameters.ticket_update_by_subject:
1146               
1147            SUBJECT_RE = re.compile(r'^(?:(?:RE|AW|VS|SV|FW|FWD):\s*)+(.*)', re.IGNORECASE)
1148            result = SUBJECT_RE.search(subject)
1149
1150            if result:
1151                ## This is a reply
1152                #
1153                orig_subject = result.group(1)
1154
1155                self.logger.debug('subject search string: %s' %(orig_subject))
1156
1157                cursor = self.db.cursor()
1158                summaries = [orig_subject, '%%: %s' % orig_subject]
1159
1160                ## Time resolution is in micoseconds
1161                #
1162                search_date = datetime.now(util.datefmt.utc) - timedelta(days=self.parameters.ticket_update_by_subject_lookback)
1163
1164                if self.VERSION < 0.12:
1165                    lookback = util.datefmt.to_timestamp(search_date)
1166                else:
1167                    lookback = util.datefmt.to_utimestamp(search_date)
1168
1169                for summary in summaries:
1170                    self.logger.debug('Looking for summary matching: "%s"' % summary)
1171
1172                    sql = """SELECT id, reporter FROM ticket
1173                            WHERE changetime >= %s AND summary LIKE %s
1174                            ORDER BY changetime DESC"""
1175
1176                    cursor.execute(sql, [lookback, summary.strip()])
1177
1178                    for row in cursor:
1179
1180                        (matched_id, sql_reporter) = row
1181
1182                        ## Save first entry.
1183                        #
1184                        if not found_id:
1185                            found_id = matched_id
1186                           
1187                        ## If subject and reporter are the same. The we certainly have found the right ticket
1188                        #
1189                        if sql_reporter == self.author:
1190                            self.logger.debug('Found matching reporter: %s with ticket id: %d' %(sql_reporter, matched_id))
1191                            found_id = matched_id
1192                            break
1193
1194                    if found_id:
1195                        self.logger.debug('Found matching ticket id: %d' % found_id)
1196                        found_id = '#%d' % found_id
1197                        return (found_id, orig_subject)
1198                   
1199                    # obsolete !?? 12 Aug 2011
1200                    #subject = orig_subject
1201
1202        return (found_id, subject)
1203
1204
1205    def new_ticket(self, msg, subject, spam, set_fields = None):
1206        """
1207        Create a new ticket
1208        """
1209        self.logger.debug('function new_ticket')
1210
1211        tkt = Ticket(self.env)
1212
1213        ## self.author can be email address of an username
1214        #
1215        tkt['reporter'] = self.author
1216
1217        self.set_cc_fields(tkt, msg)
1218
1219        self.set_ticket_fields(tkt)
1220
1221        ## Check the permission of the reporter
1222        #
1223        if self.parameters.ticket_permission_system:
1224            if not self.check_permission(tkt, 'TICKET_CREATE'):
1225                self.logger.info('Reporter: %s has no permission to create tickets' %self.author)
1226                return False
1227
1228        ## Old style setting for component, will be removed
1229        #
1230        if spam:
1231            tkt['component'] = 'Spam'
1232
1233        elif self.parameters.has_key('component'):
1234            tkt['component'] = self.parameters['component']
1235
1236        if not msg['Subject']:
1237            tkt['summary'] = u'(No subject)'
1238        else:
1239            tkt['summary'] = subject
1240
1241
1242        if set_fields:
1243            rest, keywords = string.split(set_fields, '?')
1244
1245            if keywords:
1246                update_fields = self.str_to_dict(keywords)
1247                self.update_ticket_fields(tkt, update_fields)
1248
1249
1250        message_parts = self.get_message_parts(msg, True)
1251
1252        ## Must we update some ticket fields properties via body_text
1253        #
1254        if self.properties:
1255                self.update_ticket_fields(tkt, self.properties)
1256
1257        message_parts = self.unique_attachment_names(message_parts)
1258       
1259        ## produce e-mail like header
1260        #
1261        head = ''
1262        if self.parameters.email_header:
1263            head = self.email_header_txt(msg)
1264            message_parts.insert(0, head)
1265           
1266        body_text = self.get_body_text(message_parts)
1267
1268        tkt['description'] = body_text
1269
1270        ## When is the change committed
1271        #
1272        if self.VERSION < 0.11:
1273            when = int(time.time())
1274        else:
1275            when = datetime.now(util.datefmt.utc)
1276
1277        if self.parameters.dry_run:
1278            self.logger.info('DRY_RUN: tkt.insert()')
1279        else:
1280            self.id = tkt.insert()
1281   
1282        changed = False
1283        comment = ''
1284
1285        ## some routines in trac are dependend on ticket id
1286        #  like alternate notify template
1287        #
1288        if self.parameters.alternate_notify_template:
1289            tkt['id'] = self.id
1290            changed = True
1291
1292        ## Rewrite the description if we have mailto enabled
1293        #
1294        if self.parameters.mailto_link:
1295            changed = True
1296            comment = u'\nadded mailto line\n'
1297
1298            #mailto = self.html_mailto_link( m['Subject'])
1299            mailto = self.html_mailto_link(subject)
1300
1301            tkt['description'] = u'%s\r\n%s%s\r\n' \
1302                %(head, mailto, body_text)
1303   
1304        ## Save the attachments to the ticket   
1305        #
1306        error_with_attachments =  self.attach_attachments(message_parts)
1307
1308        if error_with_attachments:
1309            changed = True
1310            comment = '%s\n%s\n' %(comment, error_with_attachments)
1311
1312        if self.parameters.email_triggers_workflow and (self.VERSION >= 0.11):
1313            if self.mail_workflow(tkt):
1314                changed = True
1315
1316        if changed:
1317            if self.parameters.dry_run:
1318                s = 'DRY_RUN: tkt.save_changes(%s, comment) real reporter = %s' %( tkt['reporter'], self.author)
1319                self.logger.info(s)
1320
1321            else:
1322                tkt.save_changes(tkt['reporter'], comment)
1323
1324        if not spam:
1325            self.notify(tkt, True)
1326
1327
1328    def attach_attachments(self, message_parts, update=False):
1329        '''
1330        save any attachments as files in the ticket's directory
1331        '''
1332        self.logger.debug('function attach_attachments()')
1333
1334        if self.parameters.dry_run:
1335            self.logger.debug("DRY_RUN: no attachments attached to tickets")
1336            return ''
1337
1338        count = 0
1339
1340        ## Get Maxium attachment size
1341        #
1342        max_size = int(self.get_config('attachment', 'max_size'))
1343        status   = None
1344       
1345        for item in message_parts:
1346            ## Skip body parts
1347            #
1348            if not isinstance(item, tuple):
1349                continue
1350               
1351            (original, filename, part) = item
1352
1353            ## We have to determine the size so we use this temporary solution.
1354            #
1355            path, fd =  util.create_unique_file(os.path.join(self.parameters.tmpdir, 'email2trac_tmp.att'))
1356            text = part.get_payload(decode=1)
1357            if not text:
1358                text = '(None)'
1359            fd.write(text)
1360            fd.close()
1361
1362            ## get the file_size
1363            #
1364            stats = os.lstat(path)
1365            file_size = stats[ST_SIZE]
1366
1367            ## Check if the attachment size is allowed
1368            #
1369            if (max_size != -1) and (file_size > max_size):
1370                status = '%s\nFile %s is larger then allowed attachment size (%d > %d)\n\n' \
1371                    %(status, original, file_size, max_size)
1372
1373                os.unlink(path)
1374                continue
1375            else:
1376                count = count + 1
1377                   
1378            ## Insert the attachment
1379            #
1380            fd = open(path, 'rb')
1381            if self.system == 'discussion':
1382                att = attachment.Attachment(self.env, 'discussion', 'topic/%s' % (self.id,))
1383            elif self.system == 'blog':
1384                att = attachment.Attachment(self.env, 'blog', '%s' % (self.id,))
1385            else:
1386                s = 'Attach %s to ticket %d' %(filename, self.id)
1387                self.print_unicode(s)
1388                att = attachment.Attachment(self.env, 'ticket', self.id)
1389 
1390            ## This will break the ticket_update system, the body_text is vaporized
1391            #  ;-(
1392            #
1393            if not update:
1394                att.author = self.author
1395                att.description = self.email_to_unicode('Added by email2trac')
1396
1397            try:
1398
1399                self.logger.debug('Insert atachment')
1400                att.insert(filename, fd, file_size)
1401
1402            except OSError, detail:
1403
1404                self.logger.info('%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail))
1405                status = '%s\nFilename %s could not be saved, problem: %s' %(status, filename, detail)
1406
1407            ## Remove the created temporary filename
1408            #
1409            fd.close()
1410            os.unlink(path)
1411
1412        ## return error
1413        #
1414        return status
1415
1416########## Fullblog functions  #################################################
1417
1418    def blog(self, msg, subject, id, params):
1419        """
1420        The blog create/update function
1421        """
1422        ## import the modules
1423        #
1424        from tracfullblog.core import FullBlogCore
1425        from tracfullblog.model import BlogPost, BlogComment
1426
1427        ## instantiate blog core
1428        #
1429        blog = FullBlogCore(self.env)
1430        req = Mock(authname='anonymous', perm=MockPerm(), args={})
1431
1432        ## parameters from the subject
1433        #
1434        params = self.str_to_dict((params or '').lstrip('?'))
1435
1436        ## preferably get the time from the email date header, or else
1437        #  use current date and time
1438        date = email.Utils.parsedate_tz(msg.get('date'))
1439        if date:
1440            dt = util.datefmt.to_datetime(email.Utils.mktime_tz(date), util.datefmt.utc)
1441        else:
1442            self.logger.warn("No valid date header found")
1443            dt = util.datefmt.to_datetime(None, util.datefmt.utc)
1444
1445        ## blog entry affected
1446        #
1447        self.id = id or util.datefmt.format_datetime(dt, "%Y%m%d%H%M%S", util.datefmt.utc)
1448
1449        ## check wether a blog post exists
1450        #
1451        post = BlogPost(self.env, self.id)
1452        force_update = self.properties.get('update', params.get('update'))
1453
1454        ## message parts
1455        #
1456        message_parts = self.get_message_parts(msg)
1457        message_parts = self.unique_attachment_names(message_parts)
1458
1459        if post.get_versions() and not force_update:
1460
1461            ## add comment to blog entry
1462            #
1463            comment = BlogComment(self.env, self.id)
1464            comment.author = self.properties.get('author', params.get('author', self.author))
1465            comment.comment = self.get_body_text(message_parts)
1466            comment.time = dt
1467
1468            if self.parameters.dry_run:
1469                self.logger.info('DRY-RUN: not adding comment for blog entry "%s"' % id)
1470                return
1471            warnings = blog.create_comment(req, comment)
1472
1473        else:
1474            ## create or update blog entry
1475            #
1476            post.author = self.properties.get('author', params.get('author', self.author))
1477            post.categories = self.properties.get('categories', params.get('categories', ''))
1478            post.title = subject.strip()
1479            post.publish_time = dt
1480            post.body = self.get_body_text(message_parts)
1481           
1482            if self.parameters.dry_run:
1483                self.logger.info('DRY-RUN: not creating blog entry "%s"' % post.title)
1484                return
1485            warnings = blog.create_post(req, post, self.author, u'Created by email2trac', False)
1486
1487        ## check for problems
1488        #
1489        if warnings:
1490            raise TracError(', '.join('blog:%s:%s' % (w[0], w[1]) for w in warnings))
1491       
1492        ## all seems well, attach attachments
1493        #
1494        self.attach_attachments(message_parts)
1495
1496
1497########## Discussion functions  ##############################################
1498
1499    def discussion_topic(self, content, subject):
1500
1501        ## Import modules.
1502        #
1503        from tracdiscussion.api import DiscussionApi
1504        from trac.util.datefmt import to_timestamp, utc
1505
1506        self.logger.debug('Creating a new topic in forum:', self.id)
1507
1508        ## Get dissussion API component.
1509        #
1510        api = self.env[DiscussionApi]
1511        context = self._create_context(content, subject)
1512
1513        ## Get forum for new topic.
1514        #
1515        forum = api.get_forum(context, self.id)
1516
1517        if not forum:
1518            self.logger.error("ERROR: Replied forum doesn't exist")
1519
1520        ## Prepare topic.
1521        #
1522        topic = {'forum' : forum['id'],
1523                 'subject' : context.subject,
1524                 'time': to_timestamp(datetime.now(utc)),
1525                 'author' : self.author,
1526                 'subscribers' : [self.email_addr],
1527                 'body' : self.get_body_text(context.content_parts)}
1528
1529        ## Add topic to DB and commit it.
1530        #
1531        self._add_topic(api, context, topic)
1532        self.db.commit()
1533
1534    def discussion_topic_reply(self, content, subject):
1535
1536        ## Import modules.
1537        #
1538        from tracdiscussion.api import DiscussionApi
1539        from trac.util.datefmt import to_timestamp, utc
1540
1541        self.logger.debug('Replying to discussion topic', self.id)
1542
1543        ## Get dissussion API component.
1544        #
1545        api = self.env[DiscussionApi]
1546        context = self._create_context(content, subject)
1547
1548        ## Get replied topic.
1549        #
1550        topic = api.get_topic(context, self.id)
1551
1552        if not topic:
1553            self.logger.error("ERROR: Replied topic doesn't exist")
1554
1555        ## Prepare message.
1556        #
1557        message = {'forum' : topic['forum'],
1558                   'topic' : topic['id'],
1559                   'replyto' : -1,
1560                   'time' : to_timestamp(datetime.now(utc)),
1561                   'author' : self.author,
1562                   'body' : self.get_body_text(context.content_parts)}
1563
1564        ## Add message to DB and commit it.
1565        #
1566        self._add_message(api, context, message)
1567        self.db.commit()
1568
1569    def discussion_message_reply(self, content, subject):
1570
1571        ## Import modules.
1572        #
1573        from tracdiscussion.api import DiscussionApi
1574        from trac.util.datefmt import to_timestamp, utc
1575
1576        self.logger.debug('Replying to discussion message', self.id)
1577
1578        ## Get dissussion API component.
1579        #
1580        api = self.env[DiscussionApi]
1581        context = self._create_context(content, subject)
1582
1583        ## Get replied message.
1584        #
1585        message = api.get_message(context, self.id)
1586
1587        if not message:
1588            self.logger.error("ERROR: Replied message doesn't exist")
1589
1590        ## Prepare message.
1591        #
1592        message = {'forum' : message['forum'],
1593                   'topic' : message['topic'],
1594                   'replyto' : message['id'],
1595                   'time' : to_timestamp(datetime.now(utc)),
1596                   'author' : self.author,
1597                   'body' : self.get_body_text(context.content_parts)}
1598
1599        ## Add message to DB and commit it.
1600        #
1601        self._add_message(api, context, message)
1602        self.db.commit()
1603
1604    def _create_context(self, content, subject):
1605
1606        ## Import modules.
1607        #
1608        from trac.mimeview import Context
1609        from trac.web.api import Request
1610        from trac.perm import PermissionCache
1611
1612        ## TODO: Read server base URL from config.
1613        #  Create request object to mockup context creation.
1614        #
1615        environ = {'SERVER_PORT' : 80,
1616                   'SERVER_NAME' : 'test',
1617                   'REQUEST_METHOD' : 'POST',
1618                   'wsgi.url_scheme' : 'http',
1619                   'wsgi.input' : sys.stdin}
1620        chrome =  {'links': {},
1621                   'scripts': [],
1622                   'ctxtnav': [],
1623                   'warnings': [],
1624                   'notices': []}
1625
1626        if self.env.base_url_for_redirect:
1627            environ['trac.base_url'] = self.env.base_url
1628
1629        req = Request(environ, None)
1630        req.chrome = chrome
1631        req.tz = 'missing'
1632        req.authname = self.author
1633        req.perm = PermissionCache(self.env, self.author)
1634        req.locale = None
1635
1636        ## Create and return context.
1637        #
1638        context = Context.from_request(req)
1639        context.realm = 'discussion-email2trac'
1640        context.cursor = self.db.cursor()
1641        context.content = content
1642        context.subject = subject
1643
1644        ## Read content parts from content.
1645        #
1646        context.content_parts = self.get_message_parts(content)
1647        context.content_parts = self.unique_attachment_names(
1648          context.content_parts)
1649
1650        return context
1651
1652    def _add_topic(self, api, context, topic):
1653        context.req.perm.assert_permission('DISCUSSION_APPEND')
1654
1655        ## Filter topic.
1656        #
1657        for discussion_filter in api.discussion_filters:
1658            accept, topic_or_error = discussion_filter.filter_topic(
1659              context, topic)
1660            if accept:
1661                topic = topic_or_error
1662            else:
1663                raise TracError(topic_or_error)
1664
1665        ## Add a new topic.
1666        #
1667        api.add_topic(context, topic)
1668
1669        ## Get inserted topic with new ID.
1670        #
1671        topic = api.get_topic_by_time(context, topic['time'])
1672
1673        ## Attach attachments.
1674        #
1675        self.id = topic['id']
1676        self.attach_attachments(context.content_parts, True)
1677
1678        ## Notify change listeners.
1679        #
1680        for listener in api.topic_change_listeners:
1681            listener.topic_created(context, topic)
1682
1683    def _add_message(self, api, context, message):
1684        context.req.perm.assert_permission('DISCUSSION_APPEND')
1685
1686        ## Filter message.
1687        #
1688        for discussion_filter in api.discussion_filters:
1689            accept, message_or_error = discussion_filter.filter_message(
1690              context, message)
1691            if accept:
1692                message = message_or_error
1693            else:
1694                raise TracError(message_or_error)
1695
1696        ## Add message.
1697        #
1698        api.add_message(context, message)
1699
1700        ## Get inserted message with new ID.
1701        #
1702        message = api.get_message_by_time(context, message['time'])
1703
1704        ## Attach attachments.
1705        #
1706
1707        self.attach_attachments(context.content_parts, True)
1708
1709        ## Notify change listeners.
1710        #
1711        for listener in api.message_change_listeners:
1712            listener.message_created(context, message)
1713
1714########## MAIN function  ######################################################
1715
1716    def parse(self, fp):
1717        """
1718        """
1719        self.logger.debug('Main function parse')
1720        global m
1721
1722        m = email.message_from_file(fp)
1723       
1724        if not m:
1725            self.logger.debug('This is not a valid email message format')
1726            return
1727           
1728        ## Work around lack of header folding in Python; see http://bugs.python.org/issue4696
1729        #
1730        try:
1731            m.replace_header('Subject', m['Subject'].replace('\r', '').replace('\n', ''))
1732        except AttributeError, detail:
1733            pass
1734
1735        if self.parameters.debug:     # save the entire e-mail message text
1736            self.save_email_for_debug(m, self.parameters.project_name, True)
1737
1738        if self.VERSION <= 0.11:
1739            self.db = self.env.get_db_cnx()
1740        else:
1741            self.db = self.env.get_read_db()
1742
1743        self.get_sender_info(m)
1744
1745
1746        if self.parameters.white_list_file:
1747            self.acl_list_from_file(self.parameters.white_list_file, 'white_list')
1748
1749        if not ( self.email_header_acl('white_list', self.email_addr, True) or self.allow_registered_user ) :
1750
1751            self.logger.info('Message rejected : %s not in white list' %(self.email_addr))
1752            return False
1753
1754        if self.email_header_acl('black_list', self.email_addr, False):
1755            self.logger.info('Message rejected : %s in black list' %(self.email_addr))
1756            return False
1757
1758        if not self.email_header_acl('recipient_list', self.email_to_addrs, True):
1759            self.logger.info('Message rejected : %s not in recipient list' %(self.email_to_addrs))
1760            return False
1761
1762        ## If spam drop the message
1763        #
1764        if self.spam(m) == 'drop':
1765            return False
1766
1767        elif self.spam(m) == 'spam':
1768            spam_msg = True
1769        else:
1770            spam_msg = False
1771
1772        if not m['Subject']:
1773            subject  = 'No Subject'
1774        else:
1775            subject  = self.email_to_unicode(m['Subject'])
1776
1777        ## Bug in python logging module <2.6
1778        #
1779        self.logger.info('subject: %s' %repr(subject))
1780
1781        ## Ticket id is in Delivered-To Field. Trac notify must send this
1782        #
1783        #try:
1784        #    print self.parameters.project_name
1785        #    print 'Delivered To', m['Delivered-To']
1786        #    print self.parameters.to_parse_address_delimiter
1787        #    id = m['Delivered-To'].split(self.parameters.to_parse_address_delimiter)[1]
1788        #    print id
1789        #    id = id.split('@')[0]
1790        #    print id
1791#
1792#        except KeyError, detail:
1793#            pass
1794
1795
1796        ## [hic] #1529: Re: LRZ
1797        #  [hic] #1529?owner=bas,priority=medium: Re: LRZ
1798        #
1799        ticket_regex = r'''
1800            (?P<new_fields>[#][?].*)
1801            |(?P<reply>(?P<id>[#][\d]+)(?P<fields>\?.*)?:)
1802            '''
1803        ## Check if  FullBlogPlugin is installed
1804        #
1805        blog_enabled = None
1806        blog_regex = ''
1807        if self.get_config('components', 'tracfullblog.*') in ['enabled']:
1808            self.logger.debug('Trac BLOG support enabled')
1809            blog_enabled = True
1810            blog_regex = '''|(?P<blog>blog(?P<blog_params>[?][^:]*)?:(?P<blog_id>\S*))'''
1811
1812
1813        ## Check if DiscussionPlugin is installed
1814        #
1815        discussion_enabled = None
1816        discussion_regex = ''
1817        if self.get_config('components', 'tracdiscussion.api.discussionapi') in ['enabled']:
1818            self.logger.debug('Trac Discussion support enabled')
1819            discussion_enabled = True
1820            discussion_regex = r'''
1821            |(?P<forum>Forum[ ][#](?P<forum_id>\d+)[ ]-[ ]?)
1822            |(?P<topic>Topic[ ][#](?P<topic_id>\d+)[ ]-[ ]?)
1823            |(?P<message>Message[ ][#](?P<message_id>\d+)[ ]-[ ]?)
1824            '''
1825
1826
1827        regex_str = ticket_regex + blog_regex + discussion_regex
1828        SYSTEM_RE = re.compile(regex_str, re.VERBOSE)
1829
1830        ## Find out if this is a ticket, a blog or a discussion
1831        #
1832        result =  SYSTEM_RE.search(subject)
1833
1834        if result:
1835            ## update ticket + fields
1836            #
1837            if result.group('reply'):
1838                self.system = 'ticket'
1839
1840                ## Skip the last ':' character
1841                #
1842                if not self.ticket_update(m, result.group('reply')[:-1], spam_msg):
1843                    self.new_ticket(m, subject, spam_msg)
1844
1845            ## New ticket + fields
1846            #
1847            elif result.group('new_fields'):
1848                self.system = 'ticket'
1849                self.new_ticket(m, subject[:result.start('new_fields')], spam_msg, result.group('new_fields'))
1850
1851            if blog_enabled:
1852                if result.group('blog'):
1853                    self.system = 'blog'
1854                    self.blog(m, subject[result.end('blog'):], result.group('blog_id'), result.group('blog_params'))
1855
1856            if discussion_enabled:
1857                ## New topic.
1858                #
1859                if result.group('forum'):
1860                    self.system = 'discussion'
1861                    self.id = int(result.group('forum_id'))
1862                    self.discussion_topic(m, subject[result.end('forum'):])
1863
1864                ## Reply to topic.
1865                #
1866                elif result.group('topic'):
1867                    self.system = 'discussion'
1868                    self.id = int(result.group('topic_id'))
1869                    self.discussion_topic_reply(m, subject[result.end('topic'):])
1870
1871                ## Reply to topic message.
1872                #
1873                elif result.group('message'):
1874                    self.system = 'discussion'
1875                    self.id = int(result.group('message_id'))
1876                    self.discussion_message_reply(m, subject[result.end('message'):])
1877
1878        else:
1879
1880            self.system = 'ticket'
1881            (matched_id, subject) = self.ticket_update_by_subject(subject)
1882
1883            if matched_id:
1884
1885                if not self.ticket_update(m, matched_id, spam_msg):
1886                    self.new_ticket(m, subject, spam_msg)
1887
1888            else:
1889                ## No update by subject, so just create a new ticket
1890                #
1891                self.new_ticket(m, subject, spam_msg)
1892
1893
1894########## BODY TEXT functions  ###########################################################
1895
1896    def strip_signature(self, text):
1897        """
1898        Strip signature from message, inspired by Mailman software
1899        """
1900        self.logger.debug('function strip_signature: %s' %self.parameters.strip_signature_regex)
1901
1902        body = []
1903
1904        STRIP_RE = re.compile( self.parameters.strip_signature_regex )
1905        for line in text.splitlines():
1906
1907            match = STRIP_RE.match(line)
1908            if match:
1909                self.logger.debug('\t"%s "  matched, skiping rest of message' %line)
1910                break
1911
1912            body.append(line)
1913
1914        return ('\n'.join(body))
1915
1916    def reflow(self, text, delsp = 0):
1917        """
1918        Reflow the message based on the format="flowed" specification (RFC 3676)
1919        """
1920        flowedlines = []
1921        quotelevel = 0
1922        prevflowed = 0
1923
1924        for line in text.splitlines():
1925            from re import match
1926           
1927            ## Figure out the quote level and the content of the current line
1928            #
1929            m = match('(>*)( ?)(.*)', line)
1930            linequotelevel = len(m.group(1))
1931            line = m.group(3)
1932
1933            ## Determine whether this line is flowed
1934            #
1935            if line and line != '-- ' and line[-1] == ' ':
1936                flowed = 1
1937            else:
1938                flowed = 0
1939
1940            if flowed and delsp and line and line[-1] == ' ':
1941                line = line[:-1]
1942
1943            ## If the previous line is flowed, append this line to it
1944            #
1945            if prevflowed and line != '-- ' and linequotelevel == quotelevel:
1946                flowedlines[-1] += line
1947
1948            ## Otherwise, start a new line
1949            #
1950            else:
1951                flowedlines.append('>' * linequotelevel + line)
1952
1953            prevflowed = flowed
1954           
1955
1956        return '\n'.join(flowedlines)
1957
1958    def strip_quotes(self, text):
1959        """
1960        Strip quotes from message by Nicolas Mendoza
1961        """
1962        self.logger.debug('function strip_quotes: %s' %self.parameters.email_quote)
1963
1964        body = []
1965
1966        STRIP_RE = re.compile( self.parameters.email_quote )
1967
1968        for line in text.splitlines():
1969
1970            try:
1971
1972                match = STRIP_RE.match(line)
1973                if match:
1974                    print line
1975                    continue
1976
1977            except UnicodeDecodeError:
1978
1979                tmp_line = self.email_to_unicode(line)
1980
1981                match = STRIP_RE.match(tmp_line)
1982                if match:
1983                    print line
1984                    continue
1985               
1986            body.append(line)
1987
1988        return ('\n'.join(body))
1989
1990    def inline_properties(self, text):
1991        """
1992        Parse text if we use inline keywords to set ticket fields
1993        """
1994        self.logger.debug('function inline_properties')
1995
1996        properties = dict()
1997        body = list()
1998
1999        INLINE_EXP = re.compile('\s*[@]\s*(\w+)\s*:(.*)$')
2000
2001        for line in text.splitlines():
2002            match = INLINE_EXP.match(line)
2003            if match:
2004                keyword, value = match.groups()
2005
2006                if self.parameters.inline_properties_first_wins:
2007                    if keyword in self.properties.keys():
2008                        continue
2009
2010                self.properties[keyword] = value.strip()
2011                self.logger.debug('\tinline properties: %s : %s' %(keyword,value))
2012
2013            else:
2014                body.append(line)
2015               
2016        return '\n'.join(body)
2017
2018
2019    def wrap_text(self, text, replace_whitespace = False):
2020        """
2021        Will break a lines longer then given length into several small
2022        lines of size given length
2023        """
2024        import textwrap
2025
2026        LINESEPARATOR = '\n'
2027        reformat = ''
2028
2029        for s in text.split(LINESEPARATOR):
2030            tmp = textwrap.fill(s, self.parameters.use_textwrap)
2031            if tmp:
2032                reformat = '%s\n%s' %(reformat,tmp)
2033            else:
2034                reformat = '%s\n' %reformat
2035
2036        return reformat
2037
2038        # Python2.4 and higher
2039        #
2040        #return LINESEPARATOR.join(textwrap.fill(s,width) for s in str.split(LINESEPARATOR))
2041        #
2042
2043########## EMAIL attachements functions ###########################################################
2044
2045    def inline_part(self, part):
2046        """
2047        """
2048        self.logger.debug('function inline_part()')
2049
2050        return part.get_param('inline', None, 'Content-Disposition') == '' or not part.has_key('Content-Disposition')
2051
2052    def get_message_parts(self, msg, new_email=False):
2053        """
2054        parses the email message and returns a list of body parts and attachments
2055        body parts are returned as strings, attachments are returned as tuples of (filename, Message object)
2056        """
2057        self.logger.debug('function get_message_parts()')
2058
2059        message_parts = list()
2060   
2061        ALTERNATIVE_MULTIPART = False
2062
2063        for part in msg.walk():
2064            content_maintype = part.get_content_maintype()
2065            content_type =  part.get_content_type()
2066
2067            self.logger.debug('\t Message part: Main-Type: %s' % content_maintype)
2068            self.logger.debug('\t Message part: Content-Type: %s' % content_type)
2069
2070            ## Check content type
2071            #
2072            if content_type in self.STRIP_CONTENT_TYPES:
2073                self.logger.debug("\t A %s attachment named '%s' was skipped" %(content_type, part.get_filename()))
2074                continue
2075
2076            ## Catch some mulitpart execptions
2077            #
2078            if content_type == 'multipart/alternative':
2079                ALTERNATIVE_MULTIPART = True
2080                continue
2081
2082            ## Skip multipart containers
2083            #
2084            if content_maintype == 'multipart':
2085                self.logger.debug("\t Skipping multipart container")
2086                continue
2087           
2088            ## Check if this is an inline part. It's inline if there is co Cont-Disp header,
2089            #  or if there is one and it says "inline"
2090            #
2091            inline = self.inline_part(part)
2092
2093            ## Drop HTML message
2094            #
2095            if ALTERNATIVE_MULTIPART and self.parameters.drop_alternative_html_version:
2096
2097                if content_type == 'text/html':
2098                    self.logger.debug('\t Skipping alternative HTML message')
2099                    ALTERNATIVE_MULTIPART = False
2100                    continue
2101
2102            filename = part.get_filename()
2103            s = '\t unicode filename: %s' %(filename)
2104            self.print_unicode(s)
2105            self.logger.debug('\t raw filename: %s' %repr(filename))
2106
2107            filename = self.check_filename_length(filename)
2108
2109            ## Save all non plain text message as attachment
2110            #
2111            if not content_type in ['text/plain']:
2112
2113                message_parts.append( (filename, part) )
2114
2115                ## We only convert html messages
2116                #
2117                if not content_type == 'text/html':
2118                    self.logger.debug('\t Appending %s (%s)' %(repr(filename), content_type))
2119                    continue
2120
2121
2122            ## We have an text or html message
2123            #
2124            if not inline:
2125                    self.logger.debug('\t Appending %s (%s), not an inline messsage part' %(repr(filename), content_type))
2126                    message_parts.append( (filename, part) )
2127                    continue
2128               
2129            ## Try to decode message part. We have a html or plain text messafe
2130            #
2131            body_text = part.get_payload(decode=1)
2132            if not body_text:           
2133                body_text = part.get_payload(decode=0)
2134
2135            ## Try to convert html message
2136            #
2137            if content_type == 'text/html':
2138
2139                body_text = self.html_2_txt(body_text)
2140                if not body_text:
2141                    continue
2142
2143            format = email.Utils.collapse_rfc2231_value(part.get_param('Format', 'fixed')).lower()
2144            delsp = email.Utils.collapse_rfc2231_value(part.get_param('DelSp', 'no')).lower()
2145
2146            if self.parameters.reflow and not self.parameters.verbatim_format and format == 'flowed':
2147                body_text = self.reflow(body_text, delsp == 'yes')
2148
2149            if new_email and self.parameters.only_strip_on_update:
2150                self.logger.debug('Skip signature/quote stripping for new messages')
2151            else:
2152                if self.parameters.strip_signature:
2153                    body_text = self.strip_signature(body_text)
2154
2155                if self.parameters.strip_quotes:
2156                    body_text = self.strip_quotes(body_text)
2157
2158            if self.parameters.inline_properties:
2159                body_text = self.inline_properties(body_text)
2160
2161            if self.parameters.use_textwrap:
2162                body_text = self.wrap_text(body_text)
2163
2164            ## Get contents charset (iso-8859-15 if not defined in mail headers)
2165            #
2166            charset = part.get_content_charset()
2167            if not charset:
2168                charset = 'iso-8859-15'
2169
2170            try:
2171                ubody_text = unicode(body_text, charset)
2172
2173            except UnicodeError, detail:
2174                ubody_text = unicode(body_text, 'iso-8859-15')
2175
2176            except LookupError, detail:
2177                ubody_text = 'ERROR: Could not find charset: %s, please install' %(charset)
2178
2179            if self.parameters.verbatim_format:
2180                message_parts.append('{{{\r\n%s\r\n}}}' %ubody_text)
2181            else:
2182                message_parts.append('%s' %ubody_text)
2183
2184        return message_parts
2185       
2186    def unique_attachment_names(self, message_parts):
2187        """
2188        Make sure we have unique names attachments:
2189          - check if it contains illegal characters
2190          - Rename "None" filenames to "untitled-part"
2191        """
2192        self.logger.debug('function unique_attachment_names()')
2193        renamed_parts = []
2194        attachment_names = set()
2195
2196        for item in message_parts:
2197           
2198            ## If not an attachment, leave it alone
2199            #
2200            if not isinstance(item, tuple):
2201                renamed_parts.append(item)
2202                continue
2203               
2204            (filename, part) = item
2205
2206            ## If filename = None, use a default one
2207            #
2208            if filename in [ 'None']:
2209                filename = 'untitled-part'
2210                self.logger.info('\t Rename filename "None" to: %s' %filename)
2211
2212                ## Guess the extension from the content type, use non strict mode
2213                #  some additional non-standard but commonly used MIME types
2214                #  are also recognized
2215                #
2216                ext = mimetypes.guess_extension(part.get_content_type(), False)
2217                if not ext:
2218                    ext = '.bin'
2219
2220                filename = '%s%s' % (filename, ext)
2221
2222            ## Discard relative paths for windows/unix in attachment names
2223            #
2224            filename = filename.replace('\\', '_')
2225            filename = filename.replace('/', '_')
2226
2227            ## remove linefeed char
2228            #
2229            for forbidden_char in ['\r', '\n']:
2230                filename = filename.replace(forbidden_char,'')
2231
2232            ## We try to normalize the filename to utf-8 NFC if we can.
2233            #  Files uploaded from OS X might be in NFD.
2234            #  Check python version and then try it
2235            #
2236            #if sys.version_info[0] > 2 or (sys.version_info[0] == 2 and sys.version_info[1] >= 3):
2237            #   try:
2238            #       filename = unicodedata.normalize('NFC', unicode(filename, 'utf-8')).encode('utf-8') 
2239            #   except TypeError:
2240            #       pass
2241
2242            ## Make the filename unique for this ticket
2243            #
2244            num = 0
2245            unique_filename = filename
2246            dummy_filename, ext = os.path.splitext(filename)
2247
2248            while (unique_filename in attachment_names) or self.attachment_exists(unique_filename):
2249                num += 1
2250                unique_filename = "%s-%s%s" % (dummy_filename, num, ext)
2251               
2252            s = '\t Attachment with filename %s will be saved as %s' % (filename, unique_filename)
2253            self.print_unicode(s)
2254
2255            attachment_names.add(unique_filename)
2256
2257            renamed_parts.append((filename, unique_filename, part))
2258   
2259        return renamed_parts
2260           
2261           
2262    def attachment_exists(self, filename):
2263
2264        self.logger.debug("function attachment_exists")
2265
2266        s = '\t check if attachment already exists: Id : %s, Filename : %s' %(self.id, filename)
2267        self.print_unicode(s)
2268
2269        ## Do we have a valid ticket id
2270        #
2271        if not self.id:
2272            return False
2273
2274        try:
2275            if self.system == 'discussion':
2276
2277                att = attachment.Attachment(self.env, 'discussion', 'ticket/%s' % (self.id,), filename)
2278
2279            elif self.system == 'blog':
2280
2281                att = attachment.Attachment(self.env, 'blog', '%s' % (self.id,), filename)
2282
2283            else:
2284
2285                att = attachment.Attachment(self.env, 'ticket', self.id, filename)
2286
2287            return True
2288
2289        except attachment.ResourceNotFound:
2290
2291            return False
2292
2293########## TRAC Ticket Text ###########################################################
2294           
2295    def get_body_text(self, message_parts):
2296        """
2297        """
2298        self.logger.debug('function get_body_text()')
2299
2300        body_text = []
2301       
2302        for part in message_parts:
2303       
2304            ## Plain text part, append it
2305            #
2306            if not isinstance(part, tuple):
2307                body_text.extend(part.strip().splitlines())
2308                body_text.append("")
2309                continue
2310
2311            (original, filename, part) = part
2312            inline = self.inline_part(part)
2313
2314            ## Skip generation of attachment link if html is converted to text
2315            #
2316            if part.get_content_type() == 'text/html' and self.parameters.html2text_cmd and inline:
2317                s = 'Skipping attachment link for html part: %s' %(filename)
2318                self.print_unicode(s)
2319                continue
2320           
2321            if part.get_content_maintype() == 'image' and inline:
2322
2323                if self.system != 'discussion':
2324                    s = 'wiki image link for: %s' %(filename)
2325                    self.print_unicode(s)
2326                    body_text.append('[[Image(%s)]]' % filename)
2327
2328                body_text.append("")
2329
2330            else:
2331
2332                if self.system != 'discussion':
2333
2334                    s = 'wiki attachment link for: %s' %(filename)
2335                    self.print_unicode(s)
2336                    body_text.append('[attachment:"%s"]' % filename)
2337
2338                body_text.append("")
2339
2340        ## Convert list body_texts to string
2341        #
2342        body_text = '\r\n'.join(body_text)
2343        return body_text
2344
2345    def html_mailto_link(self, subject):
2346        """
2347        This function returns a HTML mailto tag with the ticket id and author email address
2348        """
2349        self.logger.debug("function html_mailto_link")
2350
2351        if not self.author:
2352            author = self.email_addr
2353        else:   
2354            author = self.author
2355
2356        if not self.parameters.mailto_cc:
2357            self.parameters.mailto_cc = ''
2358           
2359        ## Bug in urllib.quote function
2360        #
2361        if isinstance(subject, unicode):
2362            subject = subject.encode('utf-8')
2363
2364        ## use urllib to escape the chars
2365        #
2366        s = '%s?Subject=%s&cc=%s' %(
2367               urllib.quote(self.email_addr),
2368               urllib.quote('Re: #%s: %s' %(self.id, subject)),
2369               urllib.quote(self.parameters.mailto_cc)
2370               )
2371
2372        if self.VERSION in [ 0.10 ]:
2373            s = '\r\n{{{\r\n#!html\r\n<a\r\n href="mailto:%s">Reply to: %s\r\n</a>\r\n}}}\r\n' %(s, author)
2374        else:
2375            s = '[mailto:"%s" Reply to: %s]' %(s, author)
2376
2377        self.logger.debug("\tmailto link %s" %s)
2378        return s
2379
2380########## TRAC notify section ###########################################################
2381
2382    def notify(self, tkt, new=True, modtime=0):
2383        """
2384        A wrapper for the TRAC notify function. So we can use templates
2385        """
2386        self.logger.debug('function notify()')
2387
2388        if self.parameters.notify_reporter:
2389            self.logger.debug('\t Notify reporter set')
2390            global sender_email
2391            sender_email = self.email_addr
2392 
2393            self.logger.debug('\t Using Email2TracNotification function AlwaysNotifyReporter')
2394            import trac.notification as Email2TracNotification
2395            Email2TracNotification.Notify.notify = AlwaysNotifyReporter
2396
2397        if self.parameters.dry_run  :
2398                self.logger.info('DRY_RUN: self.notify(tkt, True) reporter = %s' %tkt['reporter'])
2399                return
2400        try:
2401
2402            #from trac.ticket.notification import TicketNotificationSystem
2403            #tn_sys = TicketNotificationSystem(self.env)
2404            #print tn_sys
2405            #print tn_sys.__dict__
2406            #sys.exit(0)
2407
2408            ## create false {abs_}href properties, to trick Notify()
2409            #
2410            if self.VERSION in [0.10]:
2411                self.env.abs_href = Href(self.get_config('project', 'url'))
2412                self.env.href = Href(self.get_config('project', 'url'))
2413
2414            tn = TicketNotifyEmail(self.env)
2415
2416            if self.parameters.alternate_notify_template:
2417
2418                if self.VERSION >= 0.11:
2419
2420                    from trac.web.chrome import Chrome
2421
2422                    if  self.parameters.alternate_notify_template_update and not new:
2423                        tn.template_name = self.parameters.alternate_notify_template_update
2424                    else:
2425                        tn.template_name = self.parameters.alternate_notify_template
2426
2427                    tn.template = Chrome(tn.env).load_template(tn.template_name, method='text')
2428                       
2429                else:
2430
2431                    tn.template_name = self.parameters.alternate_notify_template
2432
2433            tn.notify(tkt, new, modtime)
2434
2435        except Exception, e:
2436            self.logger.error('Failure sending notification on creation of ticket #%s: %s' %(self.id, e))
2437
2438########## END Class Definition  ########################################################
2439
2440########## Global Notificaition Function ################################################
2441def AlwaysNotifyReporter(self, resid):
2442    """
2443    Copy of def notify() to manipulate recipents to always include reporter for the
2444    notification.
2445    """
2446    #print sender_email, resid
2447    (torcpts, ccrcpts) = self.get_recipients(resid)
2448    #print "get_recipients finished"
2449
2450    if not tktparser.email_header_acl('notify_reporter_black_list', sender_email, False):
2451        ## additionally append sender (regardeless of settings in trac.ini)
2452        #
2453        if not sender_email in torcpts:
2454            torcpts.append(sender_email)
2455
2456    self.begin_send()
2457    self.send(torcpts, ccrcpts)
2458    self.finish_send()
2459
2460########## Parse Config File  ###########################################################
2461
2462def ReadConfig(file, name):
2463    """
2464    Parse the config file
2465    """
2466    if not os.path.isfile(file):
2467        print 'File %s does not exist' %file
2468        sys.exit(1)
2469
2470    config = trac_config.Configuration(file)
2471   
2472    parentdir = config.get('DEFAULT', 'parentdir')
2473    sections = config.sections()
2474
2475    ## use some trac internals to get the defaults
2476    #
2477    tmp = config.parser.defaults()
2478    project =  SaraDict()
2479
2480    for option, value in tmp.items():
2481        try:
2482            project[option] = int(value)
2483        except ValueError:
2484            project[option] = value
2485
2486    if name:
2487        if name in sections:
2488            project =  SaraDict()
2489            for option, value in  config.options(name):
2490                try:
2491                    project[option] = int(value)
2492                except ValueError:
2493                    project[option] = value
2494
2495        elif not parentdir:
2496            print "Not a valid project name: %s, valid names are: %s" %(name, sections)
2497            print "or set parentdir in the [DEFAULT] section"
2498            sys.exit(1)
2499
2500    ## If parentdir then set project dir to parentdir + name
2501    #
2502    if not project.has_key('project'):
2503        if not parentdir:
2504            print "You must set project or parentdir in your configuration file"
2505            sys.exit(1)
2506        elif not name:
2507            print "You must configure a  project section in your configuration file"
2508        else:
2509            project['project'] = os.path.join(parentdir, name)
2510
2511    ##
2512    # Save the project name
2513    #
2514    project['project_name'] = os.path.basename(project['project'])
2515
2516    return project
2517
2518########## Setup Logging ###############################################################
2519
2520def setup_log(parameters, project_name, interactive=None):
2521    """
2522    Setup logging
2523
2524    Note for log format the usage of `$(...)s` instead of `%(...)s` as the latter form
2525    would be interpreted by the ConfigParser itself.
2526    """
2527    logger = logging.getLogger('email2trac %s' %project_name)
2528
2529    if interactive:
2530        parameters.log_type = 'stderr'
2531
2532    if not parameters.log_type:
2533        if sys.platform in ['win32', 'cygwin']:
2534            parameters.log_type = 'eventlog'
2535        else:
2536            parameters.log_type = 'syslog'
2537
2538    if parameters.log_type == 'file':
2539
2540        if not parameters.log_file:
2541            parameters.log_file = 'email2trac.log'
2542
2543        if not os.path.isabs(parameters.log_file):
2544            parameters.log_file = os.path.join(tempfile.gettempdir(), parameters.log_file)
2545
2546        log_handler = logging.FileHandler(parameters.log_file)
2547
2548    elif parameters.log_type in ('winlog', 'eventlog', 'nteventlog'):
2549        ## Requires win32 extensions
2550        #
2551        logid = "email2trac"
2552        log_handler = logging.handlers.NTEventLogHandler(logid, logtype='Application')
2553
2554    elif parameters.log_type in ('syslog', 'unix'):
2555        log_handler = logging.handlers.SysLogHandler('/dev/log')
2556
2557    elif parameters.log_type in ('stderr'):
2558        log_handler = logging.StreamHandler(sys.stderr)
2559
2560    else:
2561        log_handler = logging.handlers.BufferingHandler(0)
2562
2563    if parameters.log_format:
2564        parameters.log_format = parameters.log_format.replace('$(', '%(')
2565    else:
2566        parameters.log_format = '%(name)s: %(message)s'
2567        if parameters.log_type in ('file', 'stderr'):
2568            parameters.log_format = '%(asctime)s ' + parameters.log_format
2569
2570    log_formatter = logging.Formatter(parameters.log_format)
2571    log_handler.setFormatter(log_formatter)
2572    logger.addHandler(log_handler)
2573
2574    if (parameters.log_level in ['DEBUG', 'ALL']) or (parameters.debug > 0):
2575        logger.setLevel(logging.DEBUG)
2576        parameters.debug = 1
2577
2578    elif parameters.log_level in ['INFO'] or parameters.verbose:
2579        logger.setLevel(logging.INFO)
2580
2581    elif parameters.log_level in ['WARNING']:
2582        logger.setLevel(logging.WARNING)
2583
2584    elif parameters.log_level in ['ERROR']:
2585        logger.setLevel(logging.ERROR)
2586
2587    elif parameters.log_level in ['CRITICAL']:
2588        logger.setLevel(logging.CRITICAL)
2589
2590    else:
2591        logger.setLevel(logging.INFO)
2592
2593    return logger
2594
2595
2596if __name__ == '__main__':
2597    ## Default config file
2598    #
2599    agilo = False
2600    configfile = '@email2trac_conf@'
2601    project = ''
2602    component = ''
2603    ticket_prefix = 'default'
2604    dry_run = None
2605    verbose = None
2606    debug_interactive = None
2607    virtualenv = '@virtualenv@'
2608
2609    SHORT_OPT = 'Acde:hf:np:t:v'
2610    LONG_OPT  =  ['agilo', 'component=', 'debug', 'dry-run', 'help', 'file=', 'project=', 'ticket_prefix=', 'virtualenv=', 'verbose']
2611
2612    try:
2613        opts, args = getopt.getopt(sys.argv[1:], SHORT_OPT, LONG_OPT)
2614    except getopt.error,detail:
2615        print __doc__
2616        print detail
2617        sys.exit(1)
2618   
2619    project_name = None
2620    for opt,value in opts:
2621        if opt in [ '-h', '--help']:
2622            print __doc__
2623            sys.exit(0)
2624        elif opt in ['-A', '--agilo']:
2625            agilo = True
2626        elif opt in ['-c', '--component']:
2627            component = value
2628        elif opt in ['-d', '--debug']:
2629            debug_interactive = 1
2630        elif opt in ['-E', '--virtualenv']:
2631            virtualenv = value
2632        elif opt in ['-f', '--file']:
2633            configfile = value
2634        elif opt in ['-n', '--dry-run']:
2635            dry_run = True
2636        elif opt in ['-p', '--project']:
2637            project_name = value
2638        elif opt in ['-t', '--ticket_prefix']:
2639            ticket_prefix = value
2640        elif opt in ['-v', '--verbose']:
2641            verbose = True
2642
2643    if virtualenv and os.path.exists(virtualenv):
2644        activate_this = os.path.join(virtualenv, 'bin/activate_this.py')
2645        if os.path.exists(activate_this):
2646            execfile(activate_this, dict(__file__=activate_this))
2647
2648    try:
2649        from trac import __version__ as trac_version
2650        from trac import config as trac_config
2651
2652    except ImportError, detail:
2653        print "Can not find a a valid trac installation, solutions could be:"
2654        print "\tset PYTHONPATH"
2655        print "\tuse the --virtualenv <dir> option"
2656        sys.exit(1)
2657   
2658    settings = ReadConfig(configfile, project_name)
2659
2660    ## The default prefix for ticket values in email2trac.conf
2661    #
2662    settings.ticket_prefix = ticket_prefix
2663    settings.dry_run = dry_run
2664    settings.verbose = verbose
2665
2666    if not settings.debug and debug_interactive:
2667        settings.debug = debug_interactive
2668
2669    if not settings.project:
2670        print __doc__
2671        print 'No Trac project is defined in the email2trac config file.'
2672        sys.exit(1)
2673
2674    logger = setup_log(settings, os.path.basename(settings.project), debug_interactive)
2675   
2676    if component:
2677        settings['component'] = component
2678
2679    ## We are only interested in the major versions
2680    # 0.12.3 --> 0.12
2681    # 1.0.2  --> 1.0
2682    #
2683    l = trac_version.split('.')
2684    version = '.'.join(l[0:2])
2685
2686    logger.debug("Found trac version: %s" %(version))
2687   
2688    try:
2689        if version == '0.10':
2690            from trac import attachment
2691            from trac.env import Environment
2692            from trac.ticket import Ticket
2693            from trac.web.href import Href
2694            from trac import util
2695            from trac.ticket.web_ui import TicketModule
2696
2697            #
2698            # return  util.text.to_unicode(str)
2699            #
2700            # see http://projects.edgewall.com/trac/changeset/2799
2701            from trac.ticket.notification import TicketNotifyEmail
2702            from trac import config as trac_config
2703            from trac.core import TracError
2704
2705        elif version in ['0.11', '0.12', '0.13', '1.0']:
2706            from trac import attachment
2707            from trac import config as trac_config
2708            from trac import util
2709            from trac.core import TracError
2710            from trac.env import Environment
2711            from trac.perm import PermissionSystem
2712            from trac.perm import PermissionCache
2713            from trac.test import Mock, MockPerm
2714            from trac.ticket.api import TicketSystem
2715            from trac.ticket.web_ui import TicketModule
2716            from trac.web.href import Href
2717
2718            if agilo:
2719
2720                try:
2721                    #from agilo.utils.config import AgiloConfig
2722                    #if AgiloConfig(self.env).is_agilo_enabled:
2723                    from agilo.ticket.model import Ticket
2724                except ImportError, detail:
2725                    logger.error('Could not find Trac  Agilo environemnt')
2726                    sys.exit(0)
2727
2728            else:
2729
2730                from trac.ticket import Ticket
2731
2732            #
2733            # return  util.text.to_unicode(str)
2734            #
2735            # see http://projects.edgewall.com/trac/changeset/2799
2736            from trac.ticket.notification import TicketNotifyEmail
2737
2738        else:
2739            logger.error('TRAC version %s is not supported' %version)
2740            sys.exit(0)
2741
2742        ## Must be set before environment is created
2743        #
2744        if settings.has_key('python_egg_cache'):
2745            python_egg_cache = str(settings['python_egg_cache'])
2746            os.environ['PYTHON_EGG_CACHE'] = python_egg_cache
2747
2748        if settings.debug > 0:
2749            logger.debug('Loading environment %s', settings.project)
2750
2751        try:
2752            env = Environment(settings['project'], create=0)
2753        except IOError, detail:
2754            logger.error("trac error: %s" %detail)
2755            sys.exit(0)
2756        except TracError, detail:
2757            logger.error("trac error: %s" %detail)
2758            sys.exit(0)
2759
2760        tktparser = TicketEmailParser(env, settings, logger, float(version))
2761        tktparser.parse(sys.stdin)
2762
2763    ## Catch all errors and use the logging module
2764    #
2765    except Exception, error:
2766
2767        etype, evalue, etb = sys.exc_info()
2768        for e in traceback.format_exception(etype, evalue, etb):
2769            logger.critical(e)
2770
2771        if m:
2772            tktparser.save_email_for_debug(m, settings.project_name, True)
2773
2774        sys.exit(1)
2775# EOB
Note: See TracBrowser for help on using the repository browser.