Skip to content

Commit

Permalink
Merge pull request #498 from inkhey/feature/replywithmail
Browse files Browse the repository at this point in the history
Add Reply with mail Feature
  • Loading branch information
buxx committed Nov 24, 2017
2 parents 8310436 + 70bd70f commit 62ff6ee
Show file tree
Hide file tree
Showing 10 changed files with 563 additions and 2 deletions.
2 changes: 2 additions & 0 deletions install/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@ redis==2.10.5
typing==3.5.3.0
rq==0.7.1
click==6.7
markdown==2.6.9
email_reply_parser==0.5.9
15 changes: 15 additions & 0 deletions tracim/development.ini.base
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ email.notification.activated = False
# notifications generated by a user or another one
email.notification.from.email = noreply+{user_id}@trac.im
email.notification.from.default_label = Tracim Notifications
email.notification.reply_to.email = reply+{content_id}@trac.im
email.notification.references.email = thread+{content_id}@trac.im
email.notification.content_update.template.html = %(here)s/tracim/templates/mail/content_update_body_html.mak
email.notification.content_update.template.text = %(here)s/tracim/templates/mail/content_update_body_text.mak
email.notification.created_account.template.html = %(here)s/tracim/templates/mail/created_account_body_html.mak
Expand All @@ -212,6 +214,19 @@ email.processing_mode = sync
# email.async.redis.port = 6379
# email.async.redis.db = 0

# Email reply configuration
email.reply.activated = False
email.reply.imap.server = your_imap_server
email.reply.imap.port = 993
email.reply.imap.user = your_imap_user
email.reply.imap.password = your_imap_password
email.reply.imap.folder = INBOX
email.reply.imap.use_ssl = true
# Token for communication between mail fetcher and tracim controller
email.reply.token = mysecuretoken
# Delay in seconds between each check
email.reply.check.heartbeat = 60

## Radical (CalDav server) configuration
# radicale.server.host = 0.0.0.0
# radicale.server.port = 5232
Expand Down
41 changes: 41 additions & 0 deletions tracim/tracim/config/app_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from tracim.lib.base import logger
from tracim.lib.daemons import DaemonsManager
from tracim.lib.daemons import MailSenderDaemon
from tracim.lib.daemons import MailFetcherDaemon
from tracim.lib.daemons import RadicaleDaemon
from tracim.lib.daemons import WsgiDavDaemon
from tracim.lib.system import InterruptManager
Expand Down Expand Up @@ -126,6 +127,9 @@ def start_daemons(manager: DaemonsManager):
if cfg.EMAIL_PROCESSING_MODE == CFG.CST.ASYNC:
manager.run('mail_sender', MailSenderDaemon)

if cfg.EMAIL_REPLY_ACTIVATED:
manager.run('mail_fetcher',MailFetcherDaemon)


def configure_depot():
"""Configure Depot."""
Expand Down Expand Up @@ -299,6 +303,12 @@ def __init__(self):
self.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL = tg.config.get(
'email.notification.from.default_label'
)
self.EMAIL_NOTIFICATION_REPLY_TO_EMAIL = tg.config.get(
'email.notification.reply_to.email',
)
self.EMAIL_NOTIFICATION_REFERENCES_EMAIL = tg.config.get(
'email.notification.references.email'
)
self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML = tg.config.get(
'email.notification.content_update.template.html',
)
Expand Down Expand Up @@ -344,6 +354,37 @@ def __init__(self):
None,
)

self.EMAIL_REPLY_ACTIVATED = asbool(tg.config.get(
'email.reply.activated',
False,
))

self.EMAIL_REPLY_IMAP_SERVER = tg.config.get(
'email.reply.imap.server',
)
self.EMAIL_REPLY_IMAP_PORT = tg.config.get(
'email.reply.imap.port',
)
self.EMAIL_REPLY_IMAP_USER = tg.config.get(
'email.reply.imap.user',
)
self.EMAIL_REPLY_IMAP_PASSWORD = tg.config.get(
'email.reply.imap.password',
)
self.EMAIL_REPLY_IMAP_FOLDER = tg.config.get(
'email.reply.imap.folder',
)
self.EMAIL_REPLY_CHECK_HEARTBEAT = int(tg.config.get(
'email.reply.check.heartbeat',
60,
))
self.EMAIL_REPLY_TOKEN = tg.config.get(
'email.reply.token',
)
self.EMAIL_REPLY_IMAP_USE_SSL = asbool(tg.config.get(
'email.reply.imap.use_ssl',
))

self.TRACKER_JS_PATH = tg.config.get(
'js_tracker_path',
)
Expand Down
100 changes: 100 additions & 0 deletions tracim/tracim/controllers/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import tg
import typing
from tg import request
from tg import Response
from tg import abort
from tg import RestController
from sqlalchemy.orm.exc import NoResultFound

from tracim.lib.content import ContentApi
from tracim.lib.user import UserApi
from tracim.model.data import ContentType
from tracim.config.app_cfg import CFG


class EventRestController(RestController):

@tg.expose('json')
def post(self) -> Response:
cfg = CFG.get_instance()

try:
json = request.json_body
except ValueError:
return Response(
status=400,
json_body={'msg': 'Bad json'},
)

if json.get('token', None) != cfg.EMAIL_REPLY_TOKEN:
# TODO - G.M - 2017-11-23 - Switch to status 403 ?
# 403 is a better status code in this case.
# 403 status response can't now return clean json, because they are
# handled somewhere else to return html.
return Response(
status=400,
json_body={'msg': 'Invalid token'}
)

if 'user_mail' not in json:
return Response(
status=400,
json_body={'msg': 'Bad json: user_mail is required'}
)

if 'content_id' not in json:
return Response(
status=400,
json_body={'msg': 'Bad json: content_id is required'}
)

if 'payload' not in json:
return Response(
status=400,
json_body={'msg': 'Bad json: payload is required'}
)

uapi = UserApi(None)
try:
user = uapi.get_one_by_email(json['user_mail'])
except NoResultFound:
return Response(
status=400,
json_body={'msg': 'Unknown user email'},
)
api = ContentApi(user)

try:
thread = api.get_one(json['content_id'],
content_type=ContentType.Any)
except NoResultFound:
return Response(
status=400,
json_body={'msg': 'Unknown content_id'},
)

# INFO - G.M - 2017-11-17
# When content_id is a sub-elem of a main content like Comment,
# Attach the thread to the main content.
if thread.type == ContentType.Comment:
thread = thread.parent
if thread.type == ContentType.Folder:
return Response(
status=400,
json_body={'msg': 'comment for folder not allowed'},
)
if 'content' in json['payload']:
api.create_comment(
workspace=thread.workspace,
parent=thread,
content=json['payload']['content'],
do_save=True,
)
return Response(
status=204,
)
else:
return Response(
status=400,
json_body={'msg': 'No content to add new comment'},
)
3 changes: 2 additions & 1 deletion tracim/tracim/controllers/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from tracim.controllers.previews import PreviewsController
from tracim.controllers.user import UserRestController
from tracim.controllers.workspace import UserWorkspaceRestController
from tracim.controllers.events import EventRestController
from tracim.lib import CST
from tracim.lib.base import logger
from tracim.lib.content import ContentApi
Expand Down Expand Up @@ -61,7 +62,7 @@ class RootController(StandardController):
previews = PreviewsController()

content = ContentController()

events = EventRestController()
# api
api = APIController()

Expand Down
38 changes: 38 additions & 0 deletions tracim/tracim/lib/daemons.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from tracim.lib.exceptions import AlreadyRunningDaemon

from tracim.lib.utils import get_rq_queue
from tracim.lib.email_fetcher import MailFetcher


class DaemonsManager(object):
Expand Down Expand Up @@ -151,6 +152,43 @@ def append_thread_callback(self, callback: collections.Callable) -> None:
raise NotImplementedError()


class MailFetcherDaemon(Daemon):
"""
Thread containing a daemon who fetch new mail from a mailbox and
send http request to a tracim endpoint to handle them.
"""

def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._fetcher = None # type: MailFetcher
self.ok = True

def run(self) -> None:
from tracim.config.app_cfg import CFG
cfg = CFG.get_instance()
self._fetcher = MailFetcher(
host=cfg.EMAIL_REPLY_IMAP_SERVER,
port=cfg.EMAIL_REPLY_IMAP_PORT,
user=cfg.EMAIL_REPLY_IMAP_USER,
password=cfg.EMAIL_REPLY_IMAP_PASSWORD,
use_ssl=cfg.EMAIL_REPLY_IMAP_USE_SSL,
folder=cfg.EMAIL_REPLY_IMAP_FOLDER,
delay=cfg.EMAIL_REPLY_CHECK_HEARTBEAT,
# FIXME - G.M - 2017-11-15 - proper tracim url formatting
endpoint=cfg.WEBSITE_BASE_URL + "/events",
token=cfg.EMAIL_REPLY_TOKEN,
)
self._fetcher.run()

def stop(self) -> None:
if self._fetcher:
self._fetcher.stop()

def append_thread_callback(self, callback: collections.Callable) -> None:
logger.warning('MailFetcherDaemon not implement append_thread_calback')
pass


class MailSenderDaemon(Daemon):
# NOTE: use *args and **kwargs because parent __init__ use strange
# * parameter
Expand Down

0 comments on commit 62ff6ee

Please sign in to comment.