-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