#####################################################################
#
# JTracker	A Issue Tracker
#
# This software is governed by a license. See
# LICENSE.txt for the terms of this license.
#
#####################################################################
__version__='$Revision: 1.35 $'[11:-2]

# General python imports
import os, cStringIO, rfc822, mimify, sys
from types import ListType

# Zope imports
import ExtensionClass
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Globals import InitializeClass, package_home
from Acquisition import aq_base
from AccessControl import ClassSecurityInfo
from AccessControl.SecurityManagement import getSecurityManager
from AccessControl.Permissions import view
from OFS.Folder import Folder

try:
    from Products.BTreeFolder2.BTreeFolder2 import BTreeFolder2
except ImportError:
    BTreeFolder2 = None

# JTracker package imports
from Permissions import ManageJTracker, SubmitJTrackerIssues
from Permissions import SupportJTrackerIssues
from JTrackerIssue import JTrackerIssue, manage_addJTrackerIssue
from utils import setupCatalog, initializeSecurity
from utils import NEW_ISSUE_TEMPLATE, MAIL_ERROR_TEMPLATE

_wwwdir = os.path.join(package_home(globals()), 'www')
addJTrackerForm = PageTemplateFile('addJTracker.pt', _wwwdir)


class JTrackerBase(ExtensionClass.Base):
    """ JTracker - A issue tracker """
    meta_type = 'JTracker'
    security = ClassSecurityInfo()

    _views = { 'index_html' :   { 'view_label' : 'Default View'
                                , 'view_id'    : 'index_html'
                                }
             , 'addIssueForm' : { 'view_label' : 'Add Issue Form'
                                , 'view_id'    : 'addIssueForm'
                                }
             , 'searchForm'   : { 'view_label' : 'Search Issues Form'
                                , 'view_id'    : 'searchForm'
                                }
             }

    _properties = ( { 'id'   : 'title'
                    , 'type' : 'string'
                    , 'mode' : 'w' 
                    }
                  , { 'id'   : 'abbreviation'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'description'
                    , 'type' : 'text'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'admin_account'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'admin_name'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'admin_email'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'jtracker_emailname'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'jtracker_emailaddress'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'mailhost'
                    , 'type' : 'string'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'components'
                    , 'type' : 'lines'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'request_types'
                    , 'type' : 'lines'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'filesize_limit'
                    , 'type' : 'int'
                    , 'mode' : 'w'
                    }
                  , { 'id'   : 'accept_email'
                    , 'type' : 'boolean'
                    , 'mode' : 'w'
                    }
                  )

    security.declareProtected(view, 'index_html')
    index_html = PageTemplateFile('viewJTracker.pt', _wwwdir)

    security.declareProtected(SubmitJTrackerIssues, 'addIssueForm')
    addIssueForm = PageTemplateFile('addJTrackerIssue_public.pt', _wwwdir)

    security.declareProtected(view, 'searchForm')
    searchForm = PageTemplateFile('searchJTracker.pt', _wwwdir)

    security.declareProtected(ManageJTracker, 'advancedForm')
    advancedForm = PageTemplateFile('advancedJTracker.pt', _wwwdir)


    def __init__( self
                , id
                , title=''
                , abbreviation=''
                , description=''
                , admin_account=''
                , admin_email=''
                , admin_name=''
                , jtracker_emailname=''
                , jtracker_emailaddress=''
                , mailhost=''
                , filesize_limit=0
                , traits=()
                , request_types=()
                , components=()
                ):
        """ Create a new JTracker instance """
        self.id = id
        self.title = title
        self.abbreviation = abbreviation
        self.description = description
        self.admin_account = admin_account
        self.admin_email = admin_email
        self.admin_name = admin_name
        self.mailhost = mailhost
        self.filesize_limit = int(filesize_limit)
        self.jtracker_emailname = jtracker_emailname
        self.jtracker_emailaddress = jtracker_emailaddress
        self.traits = list(traits)
        self.request_types = list(request_types)
        self.components = list(components)
        self._accounts = {}
        self.accept_email = 1


    def __bobo_traverse__(self, REQUEST, name=None):
        """ Traverse across this object """
        if name in self._views.keys():
            view_data = self._views.get(name)
            default_view = getattr(self, view_data.get('view_id'))
            vm = getattr(self, 'views_manager', None)

            if vm is None:
                return default_view

            view_ob = vm.getViewObject(self, view_data.get('view_label'))

            if view_ob is None:
                return default_view
            else:
                return view_ob

        target = getattr(self, name, None)
        if target is not None:
            return target
        else:
            raise AttributeError, name


    security.declarePrivate('manage_afterAdd')
    def manage_afterAdd(self, item, container):
        """ Processing after creation """

        # Try to find a 'MailHost' or create it if necessary
        if not getattr(self, 'mailhost', ''):
            self.manage_addProduct['MailHost'].manage_addMailHost('MailHost')
            mh = getattr(self, 'MailHost')
            mh.manage_makeChanges('Mail Host', 'localhost', 25)
            self.mailhost = '/%s/MailHost' % self.absolute_url(1)

        # Set up the internal catalog
        setupCatalog(self)

        # Initialize the security settings, add roles etc.
        initializeSecurity(self)


    security.declareProtected(ManageJTracker, 'all_meta_types')
    def all_meta_types(self):
        """ What can be put inside me? """
        mts =  [ { 'action' : 'manage_addProduct/JTracker/addJTrackerIssue'
                 , 'permission' : ManageJTracker
                 , 'name' : 'JTracker Issue'
                 , 'Product' : 'JTracker'
                 } ]

        if not hasattr(aq_base(self), 'catalog'):
            cat_action = 'manage_addProduct/ZCatalog/addZCatalog'
            mts.append( { 'action' : cat_action
                        , 'permission' : ManageJTracker
                        , 'name' : 'ZCatalog'
                        , 'Product' : 'ZCatalog'
                        } )

        if not hasattr(aq_base(self), 'MailHost'):
            mh_action = 'manage_addProduct/MailHost/addMailHost_form'
            mts.append( { 'action' : mh_action
                        , 'permission' : ManageJTracker
                        , 'name' : 'Mail Host'
                        , 'Product' : 'MailHost'
                        } )

            try:
                from Products import MaildropHost
                mdh_act = 'manage_addProduct/MaildropHost/addMaildropHost_form'
                mts.append( { 'action' : mdh_act
                            , 'permission' : ManageJTracker
                            , 'name' : 'Maildrop Host'
                            , 'Product' : 'MaildropHost'
                            } )
            except ImportError:
                pass

        return tuple(mts)


    security.declareProtected(ManageJTracker, 'getAccounts')
    def getAccounts(self):
        """ Retrieve account names known to the JTracker """
        return tuple(self._accounts.values())


    security.declareProtected(view, 'getAccountInfo')
    def getAccountInfo(self):
        """ Retrieve account info for the current user """
        user = getSecurityManager().getUser()
        uname = user.getUserName()

        if uname == self.getProperty('admin_account'):
            account = { 'user_id'   : uname
                      , 'email'     : self.getProperty('admin_email')
                      , 'full_name' : self.getProperty('admin_name')
                      }
        else:
            account = self._accounts.get(uname, None)

        if account is None:
            account = { 'user_id'   : 'Anonymous'
                      , 'email'     : ''
                      , 'full_name' : 'Anonymous Coward'
                      }

        return account


    security.declareProtected(ManageJTracker, 'addAccount')
    def addAccount(self, user_id, full_name, email, REQUEST=None):
        """ Add a new user account """
        if user_id not in self._accounts.keys():
            accounts = self._accounts
            accounts[user_id] = { 'user_id'   : user_id
                                , 'email'     : email
                                , 'full_name' : full_name
                                }
            self._accounts = accounts

            local_roles = list(self.get_local_roles_for_userid(user_id))
            if 'JTracker Supporter' not in local_roles:
                local_roles.append('JTracker Supporter')
                self.manage_setLocalRoles(user_id, local_roles)

        if REQUEST is not None:
            msg = 'User %s added' % user_id
            return self.advancedForm(manage_tabs_message=msg)


    security.declareProtected(ManageJTracker, 'deleteAccounts')
    def deleteAccounts(self, user_ids, REQUEST=None):
        """ Remove accounts """
        for user_id in user_ids:
            if self._accounts.has_key(user_id):
                del self._accounts[user_id]

                local_roles = list(self.get_local_roles_for_userid(user_id))
                if 'JTracker Supporter' in local_roles:
                    if len(local_roles) == 1:
                        self.manage_delLocalRoles((user_id,))
                    else:
                        local_roles.remove('JTracker Supporter')
                        self.manage_setLocalRoles(user_id, local_roles)

        if REQUEST is not None:
            msg = 'Users %s deleted' % ', '.join(user_ids)
            return self.advancedForm(manage_tabs_message=msg)


    security.declareProtected(ManageJTracker, 'manage_edit')
    def manage_edit(self, title):
        """ Edit the JTracker Object """
        self.title = title


    security.declareProtected(ManageJTracker, 'manage_reindex')
    def manage_reindex(self, REQUEST=None):
        """ Completely reindex all issues """
        all_issues = self.objectValues(['JTracker Issue'])
        for issue in all_issues:
            issue.indexObject()

        if REQUEST is not None:
            msg = 'Issues reindexed'
            return self.advancedForm(manage_tabs_message=msg)


    security.declareProtected(SubmitJTrackerIssues, 'addIssue')
    def addIssue( self
                , title=''
                , request_type=''
                , component=''
                , description=''
                , requester_name=''
                , requester_email=''
                , file=None
                , send_email=1
                , REQUEST=None
                ):
        """ Used to add issues from the public side """
        error = ''

        missing_values = []
        for key, val in ( { 'Title' : title
                          , 'Request type' : request_type
                          , 'Component' : component
                          , 'Description' : description
                          , 'Requester Name' : requester_name
                          , 'Requester Email' : requester_email
                          }.items() ):
            if val == '':
                missing_values.append(key)
            
        if len(missing_values) > 0:
            error = 'Missing values for: %s' % ', '.join(missing_values)

        if requester_email.find('@') == -1 or requester_email.find('.') == -1:
            error = 'Invalid email address'

        if hasattr(file, 'filename') and file.filename != '':
            file.seek(0, 2)
            kb_size = file.tell()/1024
            kb_limit = int(self.getProperty('filesize_limit'))

            if kb_size > kb_limit:
                error = 'File size too large, limit is %i KB' % kb_limit

        if error and REQUEST is not None:
            form = self.__bobo_traverse__(REQUEST, 'addIssueForm')
            return form(error_msg=error)
        elif error:
            return error

        self._addIssue( title
                      , description
                      , component
                      , request_type
                      , requester_name
                      , requester_email
                      , file
                      , send_email
                      )

        if REQUEST is not None:
            REQUEST.RESPONSE.redirect('%s/' % self.absolute_url())


    security.declarePrivate('_addIssue')
    def _addIssue( self
                 , title
                 , description
                 , component
                 , request_type
                 , requester_name
                 , requester_email
                 , file=None
                 , send_email=1
                 ):
        """ Do the actual work """
        id = manage_addJTrackerIssue( self
                                    , title=title
                                    , description=description
                                    , component=component
                                    , request_type=request_type
                                    , requester_name=requester_name
                                    , requester_email=requester_email
                                    , file=file
                                    )

        tracker_url = self.absolute_url()
        issue_url = '%s/%s' % (tracker_url, id)

        msg = NEW_ISSUE_TEMPLATE % { 'requester_name' : requester_name
                                   , 'requester_email' : requester_email
                                   , 'component' : component
                                   , 'request_type' : request_type
                                   , 'request_url' : issue_url
                                   , 'title' : title
                                   , 'description' : description
                                   , 'jtracker_title' : self.title
                                   , 'jtracker_url' : tracker_url
                                   }
        jtracker_to = '"%s" <%s>' % (self.admin_name, self.admin_email)
        requester_to = '"%s" <%s>' % (requester_name, requester_email)
        subject = '%s %s: "%s" (%s)' % (component, request_type, title, id)

        if send_email:
            if self.admin_email != requester_email:
                self.sendMail(requester_to, subject, msg)

            issue_ob = self._getOb(id)
            admin_subject = '%s %s' % (subject, issue_ob._secret)
            supporters = self.getAccounts()

            for supporter in supporters:
                try:
                    sup_name = supporter.get('full_name', '')
                    sup_email = supporter.get('email')
                    self.sendMail( '"%s" <%s>' % (sup_name, sup_email)
                                 , admin_subject
                                 , msg
                                 )
                except smtplib.SMTPException:
                    continue

            self.sendMail(jtracker_to, admin_subject, msg)


    security.declareProtected(SupportJTrackerIssues, 'forceLogin')
    def forceLogin(self, came_from, REQUEST):
        """ Force login """
        if came_from == self.absolute_url():
            came_from = '%s/' % came_from

        REQUEST.RESPONSE.redirect(came_from)


    security.declareProtected(SubmitJTrackerIssues, 'receiveMail')
    def receiveMail(self, file=None):
        """ Accept emails pushed directly into the JTracker """
        if not self.getProperty('accept_email', ''):
            return

        if file is None:
            return

        errors = []

        file_handle = cStringIO.StringIO(file)
        rfc_msg = rfc822.Message(file_handle)

        subject = mimify.mime_decode_header(rfc_msg.get('subject'))
        if not subject:
            errors.append('Message has no subject')

        low_sub = subject.lower()
        if ( low_sub.find('undeliverable:') != -1 or 
             low_sub.find('returned mail:') != -1 ):
            errors.append('Message appears to be bounce message')

        description = rfc_msg.fp.read()
        if not description:
            errors.append('Message has no body')

        req_name, req_email = rfc_msg.getaddr('from')
        subject_length = len(subject)
        component = ''
        req_type = ''

        if req_name == '':
            req_name = 'Anonymous Coward'

        for possible_type in self.getProperty('request_types', []):
            if low_sub.find(possible_type.lower()) != -1:
                req_type = possible_type
                break

        if low_sub.find('issue_') != -1: # Comment on an existing issue
            unrestricted = 0
            issue_id = 'issue_'
            i = low_sub.find('issue_') + 6

            while i < subject_length and low_sub[i].isdigit():
                issue_id = '%s%s' % (issue_id, low_sub[i])
                i += 1

            if issue_id != 'issue_':
                issue_ob = getattr(self, issue_id, None)
                reply_type = 'Comment'

                if ( hasattr(issue_ob, '_secret') and 
                     subject.find(issue_ob._secret) != -1 ):
                    # Privileged interaction possible
                    accounts = self.getAccounts()
                    account = None

                    for acc_info in accounts:
                        if req_email.lower().find(acc_info['email']) != -1:
                            account = acc_info['user_id']
                            acc_roles = self.get_local_roles_for_userid(account)
                            if SupportJTrackerIssues not in acc_roles:
                                account = None

                    reply_types = issue_ob.getReplyTypes(unrestricted=1)
                    subj_tail = low_sub[subject.find(issue_ob._secret):]
                    
                    for r_type in reply_types:
                        if subj_tail.find(r_type.lower()) != -1:
                            reply_type = r_type
                            break

                    if reply_type != 'Comment':
                        unrestricted = 1

                    if account is None and unrestricted == 1:
                        msg = '"%s" not authorized to %s %s' % ( req_email
                                                               , reply_type
                                                               , issue_id
                                                               )
                        errors.append(msg)

                if issue_ob is not None and len(errors) == 0:
                    issue_ob.addReply( reply_type=reply_type
                                     , description=description
                                     , requester_name=req_name
                                     , requester_email=req_email
                                     , unrestricted=unrestricted
                                     )

        else:   # A brand new issue
            for possible_component in self.getProperty('components', []):
                if low_sub.find(possible_component.lower()) != -1:
                    component = possible_component
                    break

            if component == '':
                errors.append('Message subject has no clues')

            if not errors:
                self._addIssue( subject
                              , description
                              , component
                              , req_type
                              , req_name
                              , req_email
                              )
        if errors:
            msg = MAIL_ERROR_TEMPLATE % { 'subject' : subject
                                        , 'description' : description
                                        , 'requester_name' : req_name
                                        , 'requester_email' : req_email
                                        , 'errors' : '\n'.join(errors)
                                        , 'jtracker_title' : self.title
                                        , 'jtracker_url' : self.absolute_url()
                                        }

            self.sendMail( '"%s" <%s>' % (self.admin_name, self.admin_email)
                         , 'Mail failure'
                         , msg
                         )


    security.declarePrivate('sendMail')
    def sendMail(self, to_addrs, subject, body):
        """ Helper to send the email messages """
        try:
            mh = self.unrestrictedTraverse(self.mailhost)
        except AttributeError:
            mh = getattr(self, 'MailHost')
            self.mailhost = '/%s' % mh.absolute_url(1)

        f = '"%s" <%s>' % (self.jtracker_emailname, self.jtracker_emailaddress)

        try:
            mh.simple_send( to_addrs
                          , f
                          , '[%s] %s' % (self.abbreviation, subject)
                          , body
                          )
        except:
            msg = MAIL_ERROR_TEMPLATE % { 'subject' : subject
                                        , 'description' : body
                                        , 'requester_name' : ''
                                        , 'requester_email' : to_addrs
                                        , 'errors' : str(sys.exc_info())
                                        , 'jtracker_title' : self.title
                                        , 'jtracker_url' : self.absolute_url()
                                        }

            mh.simple_send( '"%s" <%s>' % (self.admin_name, self.admin_email)
                          , f
                          , '[%s] Mail failure' % self.abbreviation
                          , msg
                          )

    security.declarePublic('saveSearch')
    def saveSearch(self, REQUEST):
        """ Save a search for possible later re-execution """
        if REQUEST.has_key('SESSION'):
            REQUEST.SESSION.set('jt', REQUEST.form)


    security.declarePublic('hasSearch')
    def hasSearch(self, REQUEST):
        """ See if the session has saves search data """
        has_search = 0

        if REQUEST.has_key('SESSION'):
            if REQUEST.SESSION.get('jt', default={}).has_key('do_search'):
                has_search = 1

        return has_search

    
    security.declarePublic('getSearch')
    def getSearch(self, REQUEST):
        """ Retrieve saved search data formatted as query string """
        qs_elems = []

        if REQUEST.has_key('SESSION'):
            jt_data = REQUEST.SESSION.get('jt', default={})
            for key, val in jt_data.items():
                if isinstance(val, ListType):
                    for v in val:
                        qs_elems.append('%s:list=%s' % (key, v))
                else:
                    qs_elems.append('%s=%s' % (key, val))

        qs = '&'.join(qs_elems)
        qs = qs.replace(' ', '+')

        return qs
        

InitializeClass(JTrackerBase)



if BTreeFolder2 is not None:
    class BTreeJTracker(JTrackerBase, BTreeFolder2):
        """ BTreeFolder2-based JTracker """
        manage_options = ( BTreeFolder2.manage_options[:3]
                         + ( { 'label' : 'Advanced', 'action' : 'advancedForm' 
                             , 'help' : ('JTracker', 'Advanced.stx') }, )
                         + BTreeFolder2.manage_options[3:]
                         )

        def __init__( self
                    , id
                    , title=''
                    , abbreviation=''
                    , description=''
                    , admin_account=''
                    , admin_email=''
                    , jtracker_emailname=''
                    , jtracker_emailaddress=''
                    , mailhost=''
                    , filesize_limit=0
                    , traits=()
                    , request_types=()
                    , components=()
                    ):
            """ Need to override to force call to BTreeFolder init """
            BTreeFolder2.__init__(self, id)
            JTrackerBase.__init__( self
                                 , id
                                 , title
                                 , abbreviation
                                 , description
                                 , admin_account
                                 , admin_email
                                 , jtracker_emailname
                                 , jtracker_emailaddress
                                 , mailhost
                                 , filesize_limit
                                 , traits
                                 , request_types
                                 , components
                                 )
            
    InitializeClass(BTreeJTracker)


class JTracker(JTrackerBase, Folder):
    """ Standard Folder-based JTracker """
    manage_options = ( Folder.manage_options[:3]
                     + ( { 'label' : 'Advanced', 'action' : 'advancedForm' 
                         , 'help' : ('JTracker', 'Advanced.stx') }, )
                     + Folder.manage_options[3:]
                     )

InitializeClass(JTracker)


def manage_addJTracker( self
                      , id
                      , title=''
                      , description=''
                      , abbreviation=''
                      , admin_account=''
                      , admin_email=''
                      , admin_name=''
                      , jtracker_emailname=''
                      , jtracker_emailaddress=''
                      , mailhost=''
                      , filesize_limit=0
                      , REQUEST=None
                      ):
    """ Factory used to install new JTracker instances """
    real_self = self.this()
    
    if id in real_self.objectIds():
        msg = 'Duplicate+ID+%s' % id
    else:
        if BTreeFolder2 is not None:
            jt_class = BTreeJTracker
        else:
            jt_class = JTracker

        jt = apply(jt_class, ( id
                             , title
                             , abbreviation
                             , description
                             , admin_account
                             , admin_email
                             , admin_name
                             , jtracker_emailname
                             , jtracker_emailaddress
                             , mailhost
                             , filesize_limit
                             ), {})

        self._setObject(id, jt)
        msg = 'JTracker+added'

    if REQUEST is not None:
        ret_url = '%s/manage_main' % REQUEST['URL1']
        REQUEST.RESPONSE.redirect('%s?manage_tabs_message=%s' % (ret_url, msg))


