--- /dev/null
+diff --git a/bugwarrior/services/mantisbt.py b/bugwarrior/services/mantisbt.py
+new file mode 100644
+index 0000000..e54af0d
+--- /dev/null
++++ b/bugwarrior/services/mantisbt.py
+@@ -0,0 +1,361 @@
++from builtins import filter
++import re
++import six
++
++import requests
++from jinja2 import Template
++
++from bugwarrior.config import asbool, aslist, die
++from bugwarrior.services import IssueService, Issue, ServiceClient
++
++import logging
++log = logging.getLogger(__name__)
++
++
++class MantisbtClient(ServiceClient):
++ def __init__(self, host, token):
++ self.host = host
++ self.session = requests.Session()
++ self.session.headers['Authorization'] = token
++
++ def _api_url(self, path, **context):
++ """ Build the full url to the API endpoint """
++ baseurl = "https://{}/api/rest".format(self.host)
++ return baseurl + path.format(**context)
++
++ def get_user(self):
++ return self.json_response(self.session.get(self._api_url("/users/me")))
++
++ def get_projects(self):
++ return self._getter(self._api_url("/projects"), subkey="projects")
++
++ def get_issues(self):
++ url = self._api_url("/issues?page_size=50")
++ return self._getter(url, page_size=50, subkey="issues")
++
++ def get_assigned_issues(self):
++ """ Returns all issues assigned to authenticated user.
++ """
++ url = self._api_url("/issues?page_size=50&filter_id=assigned")
++ return self._getter(url, page_size=50, subkey="issues")
++
++ def get_monitored_issues(self):
++ """ Returns all issues monitored by authenticated user.
++ """
++ url = self._api_url("/issues?page_size=50&filter_id=monitored")
++ return self._getter(url, page_size=50, subkey="issues")
++
++ def get_reported_issues(self):
++ """ Returns all issues reported by authenticated user.
++ """
++ url = self._api_url("/issues?page_size=50&filter_id=reported")
++ return self._getter(url, page_size=50, subkey="issues")
++
++ def _getter(self, url, page_size=None, subkey=None):
++ """ Pagination utility. Obnoxious. """
++
++ results = []
++ link = dict(next=url)
++ page_number = 1
++
++ while 'next' in link:
++ if page_size is not None:
++ response = self.session.get(link['next'] + "&page=" + str(page_number))
++ else:
++ response = self.session.get(link['next'])
++
++ json_res = self.json_response(response)
++
++ if subkey is not None:
++ json_res = json_res[subkey]
++
++ results += json_res
++
++ if page_size is not None and len(json_res) == page_size:
++ page_number += 1
++ else:
++ break
++
++ return results
++
++class MantisbtIssue(Issue):
++ TITLE = 'mantisbttitle'
++ BODY = 'mantisbtbody'
++ CREATED_AT = 'mantisbtcreatedon'
++ UPDATED_AT = 'mantisbtupdatedat'
++ CLOSED_AT = 'mantisbtclosedon'
++ URL = 'mantisbturl'
++ PROJECT = 'mantisbtproject'
++ NUMBER = 'mantisbtnumber'
++ USER = 'mantisbtuser'
++ CATEGORY = 'mantisbtcategory'
++ STATE = 'mantisbtstate'
++
++ UDAS = {
++ TITLE: {
++ 'type': 'string',
++ 'label': 'Mantisbt Title',
++ },
++ BODY: {
++ 'type': 'string',
++ 'label': 'Mantisbt Body',
++ },
++ CREATED_AT: {
++ 'type': 'date',
++ 'label': 'Mantisbt Created',
++ },
++ UPDATED_AT: {
++ 'type': 'date',
++ 'label': 'Mantisbt Updated',
++ },
++ CLOSED_AT: {
++ 'type': 'date',
++ 'label': 'Mantisbt Closed',
++ },
++ PROJECT: {
++ 'type': 'string',
++ 'label': 'Mantisbt Project',
++ },
++ URL: {
++ 'type': 'string',
++ 'label': 'Mantisbt URL',
++ },
++ NUMBER: {
++ 'type': 'numeric',
++ 'label': 'Mantisbt Issue #',
++ },
++ USER: {
++ 'type': 'string',
++ 'label': 'Mantisbt User',
++ },
++ CATEGORY: {
++ 'type': 'string',
++ 'label': 'Mantisbt Category',
++ },
++ STATE: {
++ 'type': 'string',
++ 'label': 'Mantisbt State',
++ }
++ }
++ UNIQUE_KEY = (URL, NUMBER, )
++
++ def _normalize_tag(self, label):
++ return re.sub(r'[^a-zA-Z0-9]', '_', label)
++
++ def to_taskwarrior(self):
++ body = self.record.get('description')
++ if body:
++ body = body.replace('\r\n', '\n')
++
++ created = self.parse_date(self.record.get('created_at'))
++ updated = self.parse_date(self.record.get('updated_at'))
++ closed_date = None
++ if self.record["status"]["name"] in ["closed", "resolved"]:
++ for history in self.record.get("history", []):
++ if history.get("field", {}).get("name", "") == "status"\
++ and history.get("new_value", {}).get("name", "") in ["closed", "resolved"]:
++ closed_date = history["created_at"]
++ closed = self.parse_date(closed_date)
++
++ return {
++ 'project': self.record['project']['name'],
++ 'priority': self.origin['default_priority'],
++ 'annotations': self.get_annotations(),
++ 'tags': self.get_tags(),
++ 'entry': created,
++ 'end': closed,
++
++ self.TITLE: self.record.get('summary'),
++ self.BODY: body,
++ self.CREATED_AT: created,
++ self.UPDATED_AT: updated,
++ self.CLOSED_AT: closed,
++ self.URL: self.get_url(),
++ self.PROJECT: self.record['project'].get('name'),
++ self.NUMBER: self.record['id'],
++ self.USER: self.record['reporter'].get('name'),
++ self.CATEGORY: self.record['category'].get('name'),
++ self.STATE: self.record['status'].get('label'),
++ }
++
++ def get_url(self):
++ return "https://{}view.php?id={}".format(self.extra['host'], self.record["id"])
++
++ def get_annotations(self):
++ annotations = []
++
++ context = self.record.copy()
++ annotation_template = Template(self.origin['annotation_template'])
++
++ for annotation_dict in self.record.get('notes', []):
++ context.update({
++ 'text': annotation_dict['text'],
++ 'date': annotation_dict['created_at'],
++ 'author': annotation_dict['reporter'].get('name', 'unknown'),
++ 'view': annotation_dict['view_state']['label'],
++ })
++ annotations.append(
++ annotation_template.render(context)
++ )
++ return annotations
++
++ def get_tags(self):
++ tags = []
++
++ context = self.record.copy()
++ tag_template = Template(self.origin['tag_template'])
++
++ for tag_dict in self.record.get('tags', []):
++ context.update({
++ 'tag': self._normalize_tag(tag_dict['name'])
++ })
++ tags.append(
++ tag_template.render(context)
++ )
++
++ return tags
++
++ def get_default_description(self):
++ return self.build_default_description(
++ title=self.record['summary'],
++ url=self.get_processed_url(self.get_url()),
++ number=self.record['id'],
++ )
++
++
++class MantisbtService(IssueService):
++ ISSUE_CLASS = MantisbtIssue
++ CONFIG_PREFIX = 'mantisbt'
++
++ def __init__(self, *args, **kw):
++ super(MantisbtService, self).__init__(*args, **kw)
++
++ self.host = self.config.get('host', 'www.mantisbt.org/bugs/')
++
++ token = self.get_password('token')
++
++ self.client = MantisbtClient(self.host, token)
++ self.user = None
++
++ self.exclude_projects = self.config.get('exclude_projects', [], aslist)
++ self.include_projects = self.config.get('include_projects', [], aslist)
++
++ self.involved_issues = self.config.get(
++ 'involved_issues', default=True, to_type=asbool
++ )
++ self.assigned_issues = self.config.get(
++ 'assigned_issues', default=False, to_type=asbool
++ )
++ self.monitored_issues = self.config.get(
++ 'monitored_issues', default=False, to_type=asbool
++ )
++ self.reported_issues = self.config.get(
++ 'reported_issues', default=False, to_type=asbool
++ )
++ self.tag_template = self.config.get(
++ 'tag_template', default='{{tag}}', to_type=six.text_type
++ )
++ self.annotation_template = self.config.get(
++ 'annotation_template', default='{{date}} {{author}} ({{view}}): {{text}}', to_type=six.text_type
++ )
++
++ def get_service_metadata(self):
++ return {
++ 'tag_template': self.tag_template,
++ 'annotation_template': self.annotation_template,
++ }
++
++ def filter_involved_issues(self, issue):
++ _, issue = issue
++ user = self.client.get_user()
++ uid = user["id"]
++ if issue["reporter"]["id"] != uid and \
++ issue.get("handler", {}).get("id") != uid and \
++ all([ x.get("user", {}).get("id") != uid for x in issue.get("history", [])]) and \
++ all([ x.get("user", {}).get("id") != uid for x in issue.get("monitors", [])]):
++ return False
++ return self.filter_project_name(issue["project"]["name"])
++
++ def filter_issues(self, issue):
++ _, issue = issue
++ return self.filter_project_name(issue["project"]["name"])
++
++ def filter_project_name(self, name):
++ if self.exclude_projects:
++ if name in self.exclude_projects:
++ return False
++
++ if self.include_projects:
++ if name in self.include_projects:
++ return True
++ else:
++ return False
++
++ return True
++
++ @staticmethod
++ def get_keyring_service(service_config):
++ host = service_config.get('host', 'www.mantisbt.org/bugs/')
++ username = service_config.get('username', default='nousername')
++ return "mantisbt://{username}@{host}".format(username=username,
++ host=host)
++
++ @staticmethod
++ def to_issue_dict(issues):
++ return { i['id']: i for i in issues }
++
++ def get_owner(self, issue):
++ return issue.get("handler", {}).get("name")
++
++ def get_author(self, issue):
++ return issue.get("reporter", {}).get("name")
++
++ def issues(self):
++ issues = {}
++ is_limited = self.assigned_issues or self.monitored_issues or self.reported_issues
++
++ if self.assigned_issues:
++ issues.update(
++ filter(self.filter_issues, self.to_issue_dict(self.client.get_assigned_issues()).items())
++ )
++ if self.monitored_issues:
++ issues.update(
++ filter(self.filter_issues, self.to_issue_dict(self.client.get_monitored_issues()).items())
++ )
++ if self.reported_issues:
++ issues.update(
++ filter(self.filter_issues, self.to_issue_dict(self.client.get_reported_issues()).items())
++ )
++
++ if not is_limited:
++ all_issues = self.to_issue_dict(self.client.get_issues())
++ if self.involved_issues:
++ issues.update(
++ filter(self.filter_involved_issues, all_issues.items())
++ )
++ else:
++ issues.update(
++ filter(self.filter_issues, all_issues.items())
++ )
++
++ log.debug(" Found %i issues.", len(issues))
++ if not is_limited:
++ issues = list(filter(self.include, issues.values()))
++ else:
++ issues = list(issues.values())
++ log.debug(" Pruned down to %i issues.", len(issues))
++
++ for issue in issues:
++ issue_obj = self.get_issue_for_record(issue)
++ extra = {
++ 'host': self.host
++ }
++ issue_obj.update_extra(extra)
++ yield issue_obj
++
++ @classmethod
++ def validate_config(cls, service_config, target):
++ if 'token' not in service_config:
++ die("[%s] has no 'mantisbt.token'" % target)
++
++ super(MantisbtService, cls).validate_config(service_config, target)
+diff --git a/setup.py b/setup.py
+index d6d957a..665e36e 100644
+--- a/setup.py
++++ b/setup.py
+@@ -80,6 +80,7 @@ setup(name='bugwarrior',
+ activecollab2=bugwarrior.services.activecollab2:ActiveCollab2Service
+ activecollab=bugwarrior.services.activecollab:ActiveCollabService
+ jira=bugwarrior.services.jira:JiraService
++ mantisbt=bugwarrior.services.mantisbt:MantisbtService
+ megaplan=bugwarrior.services.megaplan:MegaplanService
+ phabricator=bugwarrior.services.phab:PhabricatorService
+ versionone=bugwarrior.services.versionone:VersionOneService