From f63bd4ff4704c9f7b6e23c76720dbd955a60c058 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Apr 2016 13:02:01 +0100 Subject: Send a rather basic email notif Also pep8 fixes --- synapse/config/emailconfig.py | 62 +++++++++++++++++++++++++++++++++++++++++++ synapse/config/homeserver.py | 3 ++- 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 synapse/config/emailconfig.py (limited to 'synapse/config') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py new file mode 100644 index 0000000000..978826627b --- /dev/null +++ b/synapse/config/emailconfig.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Copyright 2015, 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This file can't be called email.py because if it is, we cannot: +import email.utils + +from ._base import Config + + +class EmailConfig(Config): + """ + Email Configuration + """ + + def read_config(self, config): + email_config = config.get("email", None) + if email_config: + self.email_enable_notifs = email_config.get("enable_notifs", True) + if ( + "smtp_host" not in email_config or + "smtp_port" not in email_config or + "notif_from" not in email_config + ): + raise RuntimeError( + "You must set smtp_host, smtp_port and notif_from " + "to send email notifications" + ) + + self.email_smtp_host = email_config["smtp_host"] + self.email_smtp_port = email_config["smtp_port"] + self.email_notif_from = email_config["notif_from"] + + # make sure it's valid + parsed = email.utils.parseaddr(self.email_notif_from) + if parsed[1] == '': + raise RuntimeError("Invalid notif_from address") + else: + self.email_enable_notifs = False + self.email_smtp_host = None + self.email_smtp_port = None + self.email_notif_from = None + + def default_config(self, config_dir_path, server_name, **kwargs): + return """ + # Enable sending emails for notification events + #email_config: + # enable_notifs: false + # smtp_host: "localhost" + # smtp_port: 25 + """ diff --git a/synapse/config/homeserver.py b/synapse/config/homeserver.py index 9a80ac39ec..fc2445484c 100644 --- a/synapse/config/homeserver.py +++ b/synapse/config/homeserver.py @@ -31,13 +31,14 @@ from .cas import CasConfig from .password import PasswordConfig from .jwt import JWTConfig from .ldap import LDAPConfig +from .emailconfig import EmailConfig class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig, RatelimitConfig, ContentRepositoryConfig, CaptchaConfig, VoipConfig, RegistrationConfig, MetricsConfig, ApiConfig, AppServiceConfig, KeyConfig, SAML2Config, CasConfig, - JWTConfig, LDAPConfig, PasswordConfig,): + JWTConfig, LDAPConfig, PasswordConfig, EmailConfig,): pass -- cgit 1.5.1 From 2ed0adb075b745e6586ca88ce7cf6b169460a7d7 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Apr 2016 18:35:29 +0100 Subject: Generate mails from a template --- synapse/config/emailconfig.py | 35 +++++++++++++++++++++++++---------- synapse/push/emailpusher.py | 12 ++++-------- synapse/push/mailer.py | 30 +++++++++++++++++------------- synapse/python_dependencies.py | 3 +++ 4 files changed, 49 insertions(+), 31 deletions(-) (limited to 'synapse/config') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 978826627b..68fb4d8060 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -28,19 +28,32 @@ class EmailConfig(Config): email_config = config.get("email", None) if email_config: self.email_enable_notifs = email_config.get("enable_notifs", True) - if ( - "smtp_host" not in email_config or - "smtp_port" not in email_config or - "notif_from" not in email_config - ): + + required = [ + "smtp_host", + "smtp_port", + "notif_from", + "template_dir", + "notif_template_html", + + ] + + missing = [] + for k in required: + if k not in email_config: + missing.append(k) + + if (len(missing) > 0): raise RuntimeError( - "You must set smtp_host, smtp_port and notif_from " - "to send email notifications" + "email.enable_notifs is True but required keys are missing: %s" % + (", ".join(["email."+k for k in missing]),) ) self.email_smtp_host = email_config["smtp_host"] self.email_smtp_port = email_config["smtp_port"] self.email_notif_from = email_config["notif_from"] + self.email_template_dir = email_config["template_dir"] + self.email_notif_template_html = email_config["notif_template_html"] # make sure it's valid parsed = email.utils.parseaddr(self.email_notif_from) @@ -48,9 +61,8 @@ class EmailConfig(Config): raise RuntimeError("Invalid notif_from address") else: self.email_enable_notifs = False - self.email_smtp_host = None - self.email_smtp_port = None - self.email_notif_from = None + # Not much point setting defaults for the rest: it would be an + # error for them to be used. def default_config(self, config_dir_path, server_name, **kwargs): return """ @@ -59,4 +71,7 @@ class EmailConfig(Config): # enable_notifs: false # smtp_host: "localhost" # smtp_port: 25 + # notif_from: Your Friendly Matrix Home Server + # template_dir: res/templates + # notif_template_html: notif.html """ diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 820c8f8467..4e21221fb7 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -61,11 +61,7 @@ class EmailPusher(object): self.processing = False if self.hs.config.email_enable_notifs: - self.mailer = Mailer( - self.store, - self.hs.config.email_smtp_host, self.hs.config.email_smtp_port, - self.hs.config.email_notif_from, - ) + self.mailer = Mailer(self.hs) else: self.mailer = None @@ -149,7 +145,7 @@ class EmailPusher(object): # *one* email updating the user on their notifications, # we then consider all previously outstanding notifications # to be delivered. - yield self.send_notification(push_action) + yield self.send_notification(unprocessed) yield self.save_last_stream_ordering_and_success(max([ ea['stream_ordering'] for ea in unprocessed @@ -252,8 +248,8 @@ class EmailPusher(object): ) @defer.inlineCallbacks - def send_notification(self, push_action): + def send_notification(self, push_actions): logger.info("Sending notif email for user %r", self.user_id) yield self.mailer.send_notification_mail( - self.user_id, self.email, push_action + self.user_id, self.email, push_actions ) diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 97cba2ec2b..0f20d43f75 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -15,34 +15,38 @@ from twisted.internet import defer -import smtplib +from twisted.mail.smtp import sendmail import email.utils import email.mime.multipart from email.mime.text import MIMEText +import jinja2 + class Mailer(object): - def __init__(self, store, smtp_host, smtp_port, notif_from): - self.store = store - self.smtp_host = smtp_host - self.smtp_port = smtp_port - self.notif_from = notif_from + def __init__(self, hs): + self.hs = hs + loader = jinja2.FileSystemLoader(self.hs.config.email_template_dir) + env = jinja2.Environment(loader=loader) + self.notif_template = env.get_template(self.hs.config.email_notif_template_html) @defer.inlineCallbacks - def send_notification_mail(self, user_id, email_address, push_action): - raw_from = email.utils.parseaddr(self.notif_from)[1] + def send_notification_mail(self, user_id, email_address, push_actions): + raw_from = email.utils.parseaddr(self.hs.config.email_notif_from)[1] raw_to = email.utils.parseaddr(email_address)[1] if raw_to == '': raise RuntimeError("Invalid 'to' address") - plainText = "yo dawg, you got notifications!" + plainText = self.notif_template.render() text_part = MIMEText(plainText, "plain") text_part['Subject'] = "New Matrix Notifications" - text_part['From'] = self.notif_from + text_part['From'] = self.hs.config.email_notif_from text_part['To'] = email_address - smtp = smtplib.SMTP(self.smtp_host, self.smtp_port) - smtp.sendmail(raw_from, raw_to, text_part.as_string()) - smtp.quit() + yield sendmail( + self.hs.config.email_smtp_host, + raw_from, raw_to, text_part.as_string(), + port=self.hs.config.email_smtp_port + ) diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index b25b736493..a065c78b4d 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -45,6 +45,9 @@ CONDITIONAL_REQUIREMENTS = { "preview_url": { "netaddr>=0.7.18": ["netaddr"], }, + "email.enable_notifs": { + "Jinja2": ["Jinja2"], + }, } -- cgit 1.5.1 From fa12209c1b297a1710f487744a8a143d6cb6a2d1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 27 Apr 2016 15:09:55 +0100 Subject: Hopefully all remaining bits for email notifs Add public facing base url to the server so synapse knows what URL to use when converting mxc to http urls for use in emails --- res/templates/notif.html | 15 ---- res/templates/notif_mail.html | 15 ++++ res/templates/room.html | 23 +++++- synapse/config/emailconfig.py | 9 ++- synapse/config/server.py | 8 ++ synapse/push/mailer.py | 166 +++++++++++++++++++++++++++++++++++------ synapse/python_dependencies.py | 1 + 7 files changed, 195 insertions(+), 42 deletions(-) delete mode 100644 res/templates/notif.html create mode 100644 res/templates/notif_mail.html (limited to 'synapse/config') diff --git a/res/templates/notif.html b/res/templates/notif.html deleted file mode 100644 index aee52ec8c9..0000000000 --- a/res/templates/notif.html +++ /dev/null @@ -1,15 +0,0 @@ - - - -
Hi {{ user_display_name }},
-
{{ summary_text }}
-
- {% for room in rooms %} - {% include 'room.html' with context %} - {% endfor %} -
- - - diff --git a/res/templates/notif_mail.html b/res/templates/notif_mail.html new file mode 100644 index 0000000000..fbfb0a767c --- /dev/null +++ b/res/templates/notif_mail.html @@ -0,0 +1,15 @@ + + + +
Hi {{ user_display_name }},
+
{{ summary_text }}
+
+ {% for room in rooms %} + {% include 'room.html' with context %} + {% endfor %} +
+ + + diff --git a/res/templates/room.html b/res/templates/room.html index ef36b4ee58..f369575b98 100644 --- a/res/templates/room.html +++ b/res/templates/room.html @@ -1,6 +1,21 @@
-

{{ room.title }}

-
- Things have happened in this room -
+

{{ room.title }}

+
+ {% if room.avatar_url %} + + {% else %} + {% if room.hash % 3 == 0 %} + + {% elif room.hash % 3 == 1 %} + + {% else %} + + {% endif %} + {% endif %} +
+
+ {% for notif in room.notifs %} + {% include 'notif.html' with context %} + {% endfor %} +
diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 68fb4d8060..893034e2ef 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -25,17 +25,19 @@ class EmailConfig(Config): """ def read_config(self, config): + self.email_enable_notifs = False + email_config = config.get("email", None) if email_config: self.email_enable_notifs = email_config.get("enable_notifs", True) + if self.email_enable_notifs: required = [ "smtp_host", "smtp_port", "notif_from", "template_dir", "notif_template_html", - ] missing = [] @@ -49,6 +51,11 @@ class EmailConfig(Config): (", ".join(["email."+k for k in missing]),) ) + if config.get("public_baseurl") is None: + raise RuntimeError( + "email.enable_notifs is True but no public_baseurl is set" + ) + self.email_smtp_host = email_config["smtp_host"] self.email_smtp_port = email_config["smtp_port"] self.email_notif_from = email_config["notif_from"] diff --git a/synapse/config/server.py b/synapse/config/server.py index df4707e1d1..19af39da70 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -28,6 +28,11 @@ class ServerConfig(Config): self.print_pidfile = config.get("print_pidfile") self.user_agent_suffix = config.get("user_agent_suffix") self.use_frozen_dicts = config.get("use_frozen_dicts", True) + self.public_baseurl = config.get("public_baseurl") + + if self.public_baseurl is not None: + if self.public_baseurl[-1] != '/': + self.public_baseurl += '/' self.listeners = config.get("listeners", []) @@ -142,6 +147,9 @@ class ServerConfig(Config): # Whether to serve a web client from the HTTP/HTTPS root resource. web_client: True + # The server's public-facing base URL + # https://example.com:8448/ + # Set the soft limit on the number of file descriptors synapse can use # Zero is used to indicate synapse should set the soft limit to the # hard limit. diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 9e2297a03b..e78c26edea 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -26,6 +26,10 @@ from synapse.types import UserID from synapse.api.errors import StoreError import jinja2 +import bleach + +import time +import urllib MESSAGE_FROM_PERSON_IN_ROOM = "You have a message from %s in the %s room" @@ -33,6 +37,27 @@ MESSAGE_FROM_PERSON = "You have a message from %s" MESSAGES_IN_ROOM = "There are some messages for you in the %s room" MESSAGES_IN_ROOMS = "Here are some messages you may have missed" +CONTEXT_BEFORE = 1 + +# From https://github.com/matrix-org/matrix-react-sdk/blob/master/src/HtmlUtils.js +ALLOWED_TAGS = [ + 'font', # custom to matrix for IRC-style font coloring + 'del', # for markdown + # deliberately no h1/h2 to stop people shouting. + 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol', + 'nl', 'li', 'b', 'i', 'u', 'strong', 'em', 'strike', 'code', 'hr', 'br', 'div', + 'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre' +] +ALLOWED_ATTRS = { + # custom ones first: + "font": ["color"], # custom to matrix + "a": ["href", "name", "target"], # remote target: custom to matrix + # We don't currently allow img itself by default, but this + # would make sense if we did + "img": ["src"], +} +ALLOWED_SCHEMES = ["http", "https", "ftp", "mailto"] + class Mailer(object): def __init__(self, hs): @@ -41,6 +66,8 @@ class Mailer(object): self.state_handler = self.hs.get_state_handler() loader = jinja2.FileSystemLoader(self.hs.config.email_template_dir) env = jinja2.Environment(loader=loader) + env.filters["format_ts"] = format_ts_filter + env.filters["mxc_to_http"] = self.mxc_to_http_filter self.notif_template = env.get_template(self.hs.config.email_notif_template_html) @defer.inlineCallbacks @@ -55,6 +82,10 @@ class Mailer(object): [pa['room_id'] for pa in push_actions] ) + notif_events = yield self.store.get_events( + [pa['event_id'] for pa in push_actions] + ) + notifs_by_room = {} for pa in push_actions: notifs_by_room.setdefault(pa["room_id"], []).append(pa) @@ -79,14 +110,16 @@ class Mailer(object): # notifs are much realtime than sync so we can afford to wait a bit. yield concurrently_execute(_fetch_room_state, rooms_in_order, 3) - rooms = [ - self.get_room_vars( - r, user_id, notifs_by_room[r], state_by_room[r] - ) for r in rooms_in_order - ] + rooms = [] - summary_text = yield self.make_summary_text( - notifs_by_room, state_by_room, user_id + for r in rooms_in_order: + vars = yield self.get_room_vars( + r, user_id, notifs_by_room[r], notif_events, state_by_room[r] + ) + rooms.append(vars) + + summary_text = self.make_summary_text( + notifs_by_room, state_by_room, notif_events, user_id ) template_vars = { @@ -109,13 +142,72 @@ class Mailer(object): port=self.hs.config.email_smtp_port ) - def get_room_vars(self, room_id, user_id, notifs, room_state): - room_vars = {} - room_vars['title'] = calculate_room_name(room_state, user_id) - return room_vars + @defer.inlineCallbacks + def get_room_vars(self, room_id, user_id, notifs, notif_events, room_state): + room_vars = { + "title": calculate_room_name(room_state, user_id), + "hash": string_ordinal_total(room_id), # See sender avatar hash + "notifs": [], + } + + for n in notifs: + vars = yield self.get_notif_vars(n, notif_events[n['event_id']], room_state) + room_vars['notifs'].append(vars) + + defer.returnValue(room_vars) @defer.inlineCallbacks - def make_summary_text(self, notifs_by_room, state_by_room, user_id): + def get_notif_vars(self, notif, notif_event, room_state): + results = yield self.store.get_events_around( + notif['room_id'], notif['event_id'], + before_limit=CONTEXT_BEFORE, after_limit=0 + ) + + ret = { + "link": self.make_notif_link(notif), + "ts": notif['received_ts'], + "messages": [], + } + + for event in results['events_before']: + vars = self.get_message_vars(notif, event, room_state) + if vars is not None: + ret['messages'].append(vars) + + vars = self.get_message_vars(notif, notif_event, room_state) + if vars is not None: + ret['messages'].append(vars) + + defer.returnValue(ret) + + def get_message_vars(self, notif, event, room_state): + msgtype = event.content["msgtype"] + + sender_state_event = room_state[("m.room.member", event.sender)] + sender_name = name_from_member_event(sender_state_event) + sender_avatar_url = sender_state_event.content["avatar_url"] + + # 'hash' for deterministically picking default images: use + # sender_hash % the number of default images to choose from + sender_hash = string_ordinal_total(event.sender) + + ret = { + "msgtype": msgtype, + "is_historical": event.event_id != notif['event_id'], + "ts": event.origin_server_ts, + "sender_name": sender_name, + "sender_avatar_url": sender_avatar_url, + "sender_hash": sender_hash, + } + + if msgtype == "m.text": + ret["body_text_plain"] = event.content["body"] + elif msgtype == "org.matrix.custom.html": + ret["body_text_html"] = safe_markup(event.content["formatted_body"]) + + return ret + + def make_summary_text(self, notifs_by_room, state_by_room, notif_events, user_id): if len(notifs_by_room) == 1: room_id = notifs_by_room.keys()[0] sender_name = None @@ -126,29 +218,50 @@ class Mailer(object): room_name = calculate_room_name( state_by_room[room_id], user_id, fallback_to_members=False ) - event = yield self.store.get_event( - notifs_by_room[room_id][0]["event_id"] - ) + event = notif_events[notifs_by_room[room_id][0]["event_id"]] if ("m.room.member", event.sender) in state_by_room[room_id]: state_event = state_by_room[room_id][("m.room.member", event.sender)] sender_name = name_from_member_event(state_event) if sender_name is not None and room_name is not None: - defer.returnValue( - MESSAGE_FROM_PERSON_IN_ROOM % (sender_name, room_name) - ) + return MESSAGE_FROM_PERSON_IN_ROOM % (sender_name, room_name) elif sender_name is not None: - defer.returnValue(MESSAGE_FROM_PERSON % (sender_name,)) + return MESSAGE_FROM_PERSON % (sender_name,) else: room_name = calculate_room_name(state_by_room[room_id], user_id) - defer.returnValue(MESSAGES_IN_ROOM % (room_name,)) + return MESSAGES_IN_ROOM % (room_name,) else: - defer.returnValue(MESSAGES_IN_ROOMS) + return MESSAGES_IN_ROOMS - defer.returnValue("Some thing have occurred in some rooms") + def make_notif_link(self, notif): + return "https://matrix.to/%s/%s" % ( + notif['room_id'], notif['event_id'] + ) def make_unsubscribe_link(self): return "https://vector.im/#/settings" # XXX: matrix.to + def mxc_to_http_filter(self, value, width, height, resizeMethod="crop"): + if value[0:6] != "mxc://": + return "" + serverAndMediaId = value[6:] + params = { + "width": width, + "height": height, + "method": resizeMethod, + } + return "%s_matrix/media/v1/thumbnail/%s?%s" % ( + self.hs.config.public_baseurl, + serverAndMediaId, + urllib.urlencode(params) + ) + + +def safe_markup(self, raw_html): + return jinja2.Markup(bleach.linkify(bleach.clean( + raw_html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS, + protocols=ALLOWED_SCHEMES, strip=True + ))) + def deduped_ordered_list(l): seen = set() @@ -158,3 +271,12 @@ def deduped_ordered_list(l): seen.add(item) ret.append(item) return ret + +def string_ordinal_total(s): + tot = 0 + for c in s: + tot += ord(c) + return tot + +def format_ts_filter(value, format): + return time.strftime(format, time.localtime(value / 1000)) diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index 16524dbdcd..618f3c43ab 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -47,6 +47,7 @@ CONDITIONAL_REQUIREMENTS = { }, "email.enable_notifs": { "Jinja2>=2.8": ["Jinja2>=2.8"], + "bleach>=1.4.2": ["bleach>=1.4.2"], }, } -- cgit 1.5.1 From 60f86fc876bcb106842d804baac9aff4860ece3b Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 15:16:30 +0100 Subject: pep8 --- synapse/config/emailconfig.py | 2 +- synapse/push/mailer.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) (limited to 'synapse/config') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 893034e2ef..06b076e3f9 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -48,7 +48,7 @@ class EmailConfig(Config): if (len(missing) > 0): raise RuntimeError( "email.enable_notifs is True but required keys are missing: %s" % - (", ".join(["email."+k for k in missing]),) + (", ".join(["email." + k for k in missing]),) ) if config.get("public_baseurl") is None: diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index d2cf24765a..ae3e41b8ce 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -62,7 +62,7 @@ ALLOWED_ATTRS = { "img": ["src"], } # When bleach release a version with this option, we can specify schemes -#ALLOWED_SCHEMES = ["http", "https", "ftp", "mailto"] +# ALLOWED_SCHEMES = ["http", "https", "ftp", "mailto"] class Mailer(object): @@ -283,7 +283,8 @@ class Mailer(object): return MESSAGES_FROM_PERSON % ( descriptor_from_member_events([ - state_by_room[room_id][("m.room.member", s)] for s in sender_ids + state_by_room[room_id][("m.room.member", s)] + for s in sender_ids ]) ) else: @@ -346,11 +347,13 @@ def deduped_ordered_list(l): ret.append(item) return ret + def string_ordinal_total(s): tot = 0 for c in s: tot += ord(c) return tot + def format_ts_filter(value, format): return time.strftime(format, time.localtime(value / 1000)) -- cgit 1.5.1 From 40d40e470d9ff047f4255a93e40af8a9870b43cc Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 13:56:21 +0100 Subject: Send mail notifs with a plaintext part too --- res/templates/notif.txt | 12 ++++++++++++ res/templates/notif_mail.txt | 10 ++++++++++ res/templates/room.txt | 6 ++++++ synapse/config/emailconfig.py | 2 ++ synapse/push/mailer.py | 27 ++++++++++++++++++++------- 5 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 res/templates/notif.txt create mode 100644 res/templates/notif_mail.txt create mode 100644 res/templates/room.txt (limited to 'synapse/config') diff --git a/res/templates/notif.txt b/res/templates/notif.txt new file mode 100644 index 0000000000..b515f394c3 --- /dev/null +++ b/res/templates/notif.txt @@ -0,0 +1,12 @@ +{% for message in notif.messages %} +{{ message.sender_name }} ({{ message.ts|format_ts("%H:%M") }}) +{% if message.msgtype == "m.text" %} +{{ message.body_text_plain }} +{% elif message.msgtype == "m.image" %} +{{ message.body_text_plain }} +{% elif message.msgtype == "m.file" %} +{{ message.body_text_plain }} +{% endif %} +{% endfor %} + +View at {{ notif.link }} diff --git a/res/templates/notif_mail.txt b/res/templates/notif_mail.txt new file mode 100644 index 0000000000..24843042a5 --- /dev/null +++ b/res/templates/notif_mail.txt @@ -0,0 +1,10 @@ +Hi {{ user_display_name }}, + +{{ summary_text }} + +{% for room in rooms %} +{% include 'room.txt' with context %} +{% endfor %} + +You can disable these notifications at {{ unsubscribe_link }} + diff --git a/res/templates/room.txt b/res/templates/room.txt new file mode 100644 index 0000000000..999d0ae60a --- /dev/null +++ b/res/templates/room.txt @@ -0,0 +1,6 @@ +{{ room.title }} +You've been invited, join at {{ room.link }} + +{% for notif in room.notifs %} +{% include 'notif.txt' with context %} +{% endfor %} diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 06b076e3f9..b7be67f173 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -38,6 +38,7 @@ class EmailConfig(Config): "notif_from", "template_dir", "notif_template_html", + "notif_template_text", ] missing = [] @@ -61,6 +62,7 @@ class EmailConfig(Config): self.email_notif_from = email_config["notif_from"] self.email_template_dir = email_config["template_dir"] self.email_notif_template_html = email_config["notif_template_html"] + self.email_notif_template_text = email_config["notif_template_text"] # make sure it's valid parsed = email.utils.parseaddr(self.email_notif_from) diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index c53ae9a547..4fd89b3e90 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -19,6 +19,7 @@ from twisted.mail.smtp import sendmail import email.utils import email.mime.multipart from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart from synapse.util.async import concurrently_execute from synapse.util.presentable_names import ( @@ -74,7 +75,12 @@ class Mailer(object): env = jinja2.Environment(loader=loader) env.filters["format_ts"] = format_ts_filter env.filters["mxc_to_http"] = self.mxc_to_http_filter - self.notif_template = env.get_template(self.hs.config.email_notif_template_html) + self.notif_template_html = env.get_template( + self.hs.config.email_notif_template_html + ) + self.notif_template_text = env.get_template( + self.hs.config.email_notif_template_text + ) @defer.inlineCallbacks def send_notification_mail(self, user_id, email_address, push_actions): @@ -135,16 +141,23 @@ class Mailer(object): "rooms": rooms, } - plainText = self.notif_template.render(**template_vars) + html_text = self.notif_template_html.render(**template_vars) + html_part = MIMEText(html_text, "html", "utf8") + + plain_text = self.notif_template_text.render(**template_vars) + text_part = MIMEText(plain_text, "plain", "utf8") + + multipart_msg = MIMEMultipart('alternative') + multipart_msg['Subject'] = "New Matrix Notifications" + multipart_msg['From'] = self.hs.config.email_notif_from + multipart_msg['To'] = email_address + multipart_msg.attach(text_part) + multipart_msg.attach(html_part) - text_part = MIMEText(plainText, "html", "utf8") - text_part['Subject'] = "New Matrix Notifications" - text_part['From'] = self.hs.config.email_notif_from - text_part['To'] = email_address yield sendmail( self.hs.config.email_smtp_host, - raw_from, raw_to, text_part.as_string(), + raw_from, raw_to, multipart_msg.as_string(), port=self.hs.config.email_smtp_port ) -- cgit 1.5.1 From 18ce88bd2dc15cad08ca6bd3c835ca14fb3478d9 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 14:24:25 +0100 Subject: Correct default template and add text template --- synapse/config/emailconfig.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'synapse/config') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index b7be67f173..6db01fa145 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -82,5 +82,6 @@ class EmailConfig(Config): # smtp_port: 25 # notif_from: Your Friendly Matrix Home Server # template_dir: res/templates - # notif_template_html: notif.html + # notif_template_html: notif_email.html + # notif_template_text: notif_email.txt """ -- cgit 1.5.1 From 6c8957be7f2b27b753d75df2d67989e09a6d6286 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 14:25:28 +0100 Subject: Remove redundant docstring --- synapse/config/emailconfig.py | 4 ---- 1 file changed, 4 deletions(-) (limited to 'synapse/config') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 6db01fa145..761ac33112 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -20,10 +20,6 @@ from ._base import Config class EmailConfig(Config): - """ - Email Configuration - """ - def read_config(self, config): self.email_enable_notifs = False -- cgit 1.5.1 From 50484559658d06203565df5af73bb20e279faa1c Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 14:27:40 +0100 Subject: Nicer get() shorthand --- synapse/config/emailconfig.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'synapse/config') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 761ac33112..4318c6f13d 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -23,9 +23,8 @@ class EmailConfig(Config): def read_config(self, config): self.email_enable_notifs = False - email_config = config.get("email", None) - if email_config: - self.email_enable_notifs = email_config.get("enable_notifs", True) + email_config = config.get("email", {}) + self.email_enable_notifs = email_config.get("enable_notifs", True) if self.email_enable_notifs: required = [ -- cgit 1.5.1 From 4b0c3a327007b86346745fc96dafc4fd8ada2f19 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 14:30:15 +0100 Subject: Correct public_baseurl default --- synapse/config/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'synapse/config') diff --git a/synapse/config/server.py b/synapse/config/server.py index 04b9221908..0b5f462e44 100644 --- a/synapse/config/server.py +++ b/synapse/config/server.py @@ -148,8 +148,8 @@ class ServerConfig(Config): # Whether to serve a web client from the HTTP/HTTPS root resource. web_client: True - # The server's public-facing base URL - # https://example.com:8448/ + # The public-facing base URL for the client API (not including _matrix/...) + # public_baseurl: https://example.com:8448/ # Set the soft limit on the number of file descriptors synapse can use # Zero is used to indicate synapse should set the soft limit to the -- cgit 1.5.1 From 765f2b8446b99fd9d31311ec0f965e6e57fc9c44 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 14:46:18 +0100 Subject: Default enable email notifs to False --- synapse/config/emailconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse/config') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 4318c6f13d..1fa2bab3a0 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -24,7 +24,7 @@ class EmailConfig(Config): self.email_enable_notifs = False email_config = config.get("email", {}) - self.email_enable_notifs = email_config.get("enable_notifs", True) + self.email_enable_notifs = email_config.get("enable_notifs", False) if self.email_enable_notifs: required = [ -- cgit 1.5.1 From 83618d719a437ac40f1d004689da4570eb436e9c Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 19:13:52 +0100 Subject: Try imports in config --- synapse/config/emailconfig.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'synapse/config') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 1fa2bab3a0..e4d86255fa 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -27,6 +27,13 @@ class EmailConfig(Config): self.email_enable_notifs = email_config.get("enable_notifs", False) if self.email_enable_notifs: + # make sure we can import the required deps + import jinja2 + import bleach + # prevent unused warnings + jinja2 + bleach + required = [ "smtp_host", "smtp_port", -- cgit 1.5.1 From 17cbf773b971e4122a913233a8656099465b3306 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 4 May 2016 11:37:21 +0100 Subject: fix assorted typos in default config --- synapse/config/emailconfig.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'synapse/config') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index e4d86255fa..7a38680435 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -78,12 +78,12 @@ class EmailConfig(Config): def default_config(self, config_dir_path, server_name, **kwargs): return """ # Enable sending emails for notification events - #email_config: + #email: # enable_notifs: false # smtp_host: "localhost" # smtp_port: 25 # notif_from: Your Friendly Matrix Home Server # template_dir: res/templates - # notif_template_html: notif_email.html - # notif_template_text: notif_email.txt + # notif_template_html: notif_mail.html + # notif_template_text: notif_mail.txt """ -- cgit 1.5.1 From 81c2176cbadf7646fc075ca45dea4360ce7b8258 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 5 May 2016 15:54:29 +0100 Subject: fix layout; handle app naming in synapse, not jinja --- res/templates-vector/mail.css | 5 +++- res/templates-vector/notif_mail.html | 4 +-- res/templates-vector/notif_mail.txt | 2 +- res/templates/notif_mail.html | 2 +- res/templates/notif_mail.txt | 2 +- synapse/config/emailconfig.py | 5 ++++ synapse/push/mailer.py | 47 ++++++++++++++++++++++++------------ 7 files changed, 45 insertions(+), 22 deletions(-) (limited to 'synapse/config') diff --git a/res/templates-vector/mail.css b/res/templates-vector/mail.css index 642948fdc1..103c5ff8ca 100644 --- a/res/templates-vector/mail.css +++ b/res/templates-vector/mail.css @@ -85,11 +85,14 @@ body { .sender_name { margin-left: 75px; display: inline; - font-weight: bold; + font-size: 13px; + color: #a2a2a2; } .message_time { float: right; + font-size: 11px; + color: #a2a2a2; } .message_body { diff --git a/res/templates-vector/notif_mail.html b/res/templates-vector/notif_mail.html index 86e7b6e867..c49cc66548 100644 --- a/res/templates-vector/notif_mail.html +++ b/res/templates-vector/notif_mail.html @@ -8,9 +8,9 @@
- +
Hi {{ user_display_name }},
-
{{ summary_text|replace("%app%", "Vector") }}
+
{{ summary_text }}
{% for room in rooms %} diff --git a/res/templates-vector/notif_mail.txt b/res/templates-vector/notif_mail.txt index dec2e5960b..24843042a5 100644 --- a/res/templates-vector/notif_mail.txt +++ b/res/templates-vector/notif_mail.txt @@ -1,6 +1,6 @@ Hi {{ user_display_name }}, -{{ summary_text|replace("%app%", "Vector") }} +{{ summary_text }} {% for room in rooms %} {% include 'room.txt' with context %} diff --git a/res/templates/notif_mail.html b/res/templates/notif_mail.html index 0290fdea01..cc3573e65a 100644 --- a/res/templates/notif_mail.html +++ b/res/templates/notif_mail.html @@ -8,7 +8,7 @@
Hi {{ user_display_name }},
-
{{ summary_text|replace("%app%", "Matrix") }}
+
{{ summary_text }}
{% for room in rooms %} {% include 'room.html' with context %} diff --git a/res/templates/notif_mail.txt b/res/templates/notif_mail.txt index 5d5a8442f9..24843042a5 100644 --- a/res/templates/notif_mail.txt +++ b/res/templates/notif_mail.txt @@ -1,6 +1,6 @@ Hi {{ user_display_name }}, -{{ summary_text|replace("%app%", "Matrix") }} +{{ summary_text }} {% for room in rooms %} {% include 'room.txt' with context %} diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index 7a38680435..d6f4f83a14 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -65,6 +65,10 @@ class EmailConfig(Config): self.email_template_dir = email_config["template_dir"] self.email_notif_template_html = email_config["notif_template_html"] self.email_notif_template_text = email_config["notif_template_text"] + if "app_name" in email_config: + self.email_app_name = email_config["app_name"] + else: + self.email_app_name = "Matrix" # make sure it's valid parsed = email.utils.parseaddr(self.email_notif_from) @@ -83,6 +87,7 @@ class EmailConfig(Config): # smtp_host: "localhost" # smtp_port: 25 # notif_from: Your Friendly Matrix Home Server + # app_name: Matrix # template_dir: res/templates # notif_template_html: notif_mail.html # notif_template_text: notif_mail.txt diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index aab6387125..497905d811 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -39,15 +39,15 @@ import logging logger = logging.getLogger(__name__) -MESSAGE_FROM_PERSON_IN_ROOM = "You have a message on %%app%% from %(person)s " \ +MESSAGE_FROM_PERSON_IN_ROOM = "You have a message on %(app)s from %(person)s " \ "in the %s room..." -MESSAGE_FROM_PERSON = "You have a message on %%app%% from %(person)s..." -MESSAGES_FROM_PERSON = "You have messages on %%app%% from %(person)s..." -MESSAGES_IN_ROOM = "There are some messages on %%app%% for you in the %(room)s room..." -MESSAGES_IN_ROOMS = "Here are some messages on %%app%% you may have missed..." +MESSAGE_FROM_PERSON = "You have a message on %(app)s from %(person)s..." +MESSAGES_FROM_PERSON = "You have messages on %(app)s from %(person)s..." +MESSAGES_IN_ROOM = "There are some messages on %(app)s for you in the %(room)s room..." +MESSAGES_IN_ROOMS = "Here are some messages on %(app)s you may have missed..." INVITE_FROM_PERSON_TO_ROOM = "%(person)s has invited you to join the " \ - "%(room)s room on %%app%%..." -INVITE_FROM_PERSON = "%(person)s has invited you to chat on %%app%%..." + "%(room)s room on %(app)s..." +INVITE_FROM_PERSON = "%(person)s has invited you to chat on %(app)s..." CONTEXT_BEFORE = 1 CONTEXT_AFTER = 1 @@ -79,6 +79,7 @@ class Mailer(object): self.store = self.hs.get_datastore() self.state_handler = self.hs.get_state_handler() loader = jinja2.FileSystemLoader(self.hs.config.email_template_dir) + self.app_name = self.hs.config.email_app_name env = jinja2.Environment(loader=loader) env.filters["format_ts"] = format_ts_filter env.filters["mxc_to_http"] = self.mxc_to_http_filter @@ -306,10 +307,15 @@ class Mailer(object): inviter_name = name_from_member_event(inviter_member_event) if room_name is None: - return INVITE_FROM_PERSON % {"person": inviter_name} + return INVITE_FROM_PERSON % { + "person": inviter_name, + "app": self.app_name + } else: return INVITE_FROM_PERSON_TO_ROOM % { - "person": inviter_name, "room": room_name + "person": inviter_name, + "room": room_name, + "app": self.app_name, } sender_name = None @@ -322,18 +328,22 @@ class Mailer(object): if sender_name is not None and room_name is not None: return MESSAGE_FROM_PERSON_IN_ROOM % { - "person": sender_name, "room": room_name + "person": sender_name, + "room": room_name, + "app": self.app_name, } elif sender_name is not None: return MESSAGE_FROM_PERSON % { - "person": sender_name + "person": sender_name, + "app": self.app_name, } else: # There's more than one notification for this room, so just # say there are several if room_name is not None: return MESSAGES_IN_ROOM % { - "room": room_name + "room": room_name, + "app": self.app_name, } else: # If the room doesn't have a name, say who the messages @@ -347,19 +357,24 @@ class Mailer(object): "person": descriptor_from_member_events([ state_by_room[room_id][("m.room.member", s)] for s in sender_ids - ]) + ]), + "app": self.app_name, } else: # Stuff's happened in multiple different rooms - return MESSAGES_IN_ROOMS + return MESSAGES_IN_ROOMS % { + "app": self.app_name, + } def make_room_link(self, room_id): # XXX: matrix.to - return "https://vector.im/#/room/%s" % (room_id,) + # need /beta for Universal Links to work on iOS + return "https://vector.im/beta/#/room/%s" % (room_id,) def make_notif_link(self, notif): # XXX: matrix.to - return "https://vector.im/#/room/%s/%s" % ( + # need /beta for Universal Links to work on iOS + return "https://vector.im/beta/#/room/%s/%s" % ( notif['room_id'], notif['event_id'] ) -- cgit 1.5.1 From 94040b0798a7e4db88e75485906fd8a2b31b117c Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 10 May 2016 14:34:53 +0200 Subject: Add config option to not send email notifs for new users --- synapse/config/emailconfig.py | 4 ++++ synapse/rest/client/v2_alpha/register.py | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) (limited to 'synapse/config') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index d6f4f83a14..b239619c9e 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -65,6 +65,9 @@ class EmailConfig(Config): self.email_template_dir = email_config["template_dir"] self.email_notif_template_html = email_config["notif_template_html"] self.email_notif_template_text = email_config["notif_template_text"] + self.email_notifs_for_new_users = email_config.get( + "notif_for_new_users", True + ) if "app_name" in email_config: self.email_app_name = email_config["app_name"] else: @@ -91,4 +94,5 @@ class EmailConfig(Config): # template_dir: res/templates # notif_template_html: notif_mail.html # notif_template_text: notif_mail.txt + # notif_for_new_users: True """ diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index 883b1c1291..ad04383555 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -219,7 +219,10 @@ class RegisterRestServlet(RestServlet): # if email notifications are enabled (so people don't start # getting mail spam where they weren't before if email # notifs are set up on a home server) - if self.hs.config.email_enable_notifs: + if ( + self.hs.config.email_enable_notifs and + self.hs.config.email_notifs_for_new_users + ): # Pull the ID of the access token back out of the db # It would really make more sense for this to be passed # up when the access token is saved, but that's quite an -- cgit 1.5.1 From c00b484eff179257f34eeb48be98bb9435598f5e Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 10 May 2016 14:39:16 +0200 Subject: More consistent config naming --- synapse/config/emailconfig.py | 2 +- synapse/rest/client/v2_alpha/register.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'synapse/config') diff --git a/synapse/config/emailconfig.py b/synapse/config/emailconfig.py index b239619c9e..90bdd08f00 100644 --- a/synapse/config/emailconfig.py +++ b/synapse/config/emailconfig.py @@ -65,7 +65,7 @@ class EmailConfig(Config): self.email_template_dir = email_config["template_dir"] self.email_notif_template_html = email_config["notif_template_html"] self.email_notif_template_text = email_config["notif_template_text"] - self.email_notifs_for_new_users = email_config.get( + self.email_notif_for_new_users = email_config.get( "notif_for_new_users", True ) if "app_name" in email_config: diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index ad04383555..1ecc02d94d 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -221,7 +221,7 @@ class RegisterRestServlet(RestServlet): # notifs are set up on a home server) if ( self.hs.config.email_enable_notifs and - self.hs.config.email_notifs_for_new_users + self.hs.config.email_notif_for_new_users ): # Pull the ID of the access token back out of the db # It would really make more sense for this to be passed -- cgit 1.5.1 From 40aa6e8349b348802d6f87084c31c3895f728708 Mon Sep 17 00:00:00 2001 From: Negi Fazeli Date: Wed, 20 Apr 2016 16:21:40 +0200 Subject: Create user with expiry - Add unittests for client, api and handler Signed-off-by: Negar Fazeli --- synapse/api/auth.py | 3 +- synapse/config/key.py | 5 ++ synapse/config/registration.py | 6 +++ synapse/handlers/auth.py | 4 +- synapse/handlers/register.py | 53 +++++++++++++++++++++ synapse/rest/client/v1/register.py | 71 ++++++++++++++++++++++++++++ tests/api/test_auth.py | 12 ++--- tests/handlers/test_register.py | 67 ++++++++++++++++++++++++++ tests/rest/client/v1/test_register.py | 88 +++++++++++++++++++++++++++++++++++ tests/utils.py | 1 + 10 files changed, 301 insertions(+), 9 deletions(-) create mode 100644 tests/handlers/test_register.py create mode 100644 tests/rest/client/v1/test_register.py (limited to 'synapse/config') diff --git a/synapse/api/auth.py b/synapse/api/auth.py index d3e9837c81..44e38b777a 100644 --- a/synapse/api/auth.py +++ b/synapse/api/auth.py @@ -612,7 +612,8 @@ class Auth(object): def get_user_from_macaroon(self, macaroon_str): try: macaroon = pymacaroons.Macaroon.deserialize(macaroon_str) - self.validate_macaroon(macaroon, "access", False) + + self.validate_macaroon(macaroon, "access", self.hs.config.expire_access_token) user_prefix = "user_id = " user = None diff --git a/synapse/config/key.py b/synapse/config/key.py index a072aec714..6ee643793e 100644 --- a/synapse/config/key.py +++ b/synapse/config/key.py @@ -57,6 +57,8 @@ class KeyConfig(Config): seed = self.signing_key[0].seed self.macaroon_secret_key = hashlib.sha256(seed) + self.expire_access_token = config.get("expire_access_token", False) + def default_config(self, config_dir_path, server_name, is_generating_file=False, **kwargs): base_key_name = os.path.join(config_dir_path, server_name) @@ -69,6 +71,9 @@ class KeyConfig(Config): return """\ macaroon_secret_key: "%(macaroon_secret_key)s" + # Used to enable access token expiration. + expire_access_token: False + ## Signing Keys ## # Path to the signing key to sign messages with diff --git a/synapse/config/registration.py b/synapse/config/registration.py index 87e500c97a..cc3f879857 100644 --- a/synapse/config/registration.py +++ b/synapse/config/registration.py @@ -32,6 +32,7 @@ class RegistrationConfig(Config): ) self.registration_shared_secret = config.get("registration_shared_secret") + self.user_creation_max_duration = int(config["user_creation_max_duration"]) self.bcrypt_rounds = config.get("bcrypt_rounds", 12) self.trusted_third_party_id_servers = config["trusted_third_party_id_servers"] @@ -54,6 +55,11 @@ class RegistrationConfig(Config): # secret, even if registration is otherwise disabled. registration_shared_secret: "%(registration_shared_secret)s" + # Sets the expiry for the short term user creation in + # milliseconds. For instance the bellow duration is two weeks + # in milliseconds. + user_creation_max_duration: 1209600000 + # Set the number of bcrypt rounds used to generate password hash. # Larger numbers increase the work factor needed to generate the hash. # The default number of rounds is 12. diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 61fe56032a..3d36d3460e 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -521,11 +521,11 @@ class AuthHandler(BaseHandler): )) return m.serialize() - def generate_short_term_login_token(self, user_id): + def generate_short_term_login_token(self, user_id, duration_in_ms=(2 * 60 * 1000)): macaroon = self._generate_base_macaroon(user_id) macaroon.add_first_party_caveat("type = login") now = self.hs.get_clock().time_msec() - expiry = now + (2 * 60 * 1000) + expiry = now + duration_in_ms macaroon.add_first_party_caveat("time < %d" % (expiry,)) return macaroon.serialize() diff --git a/synapse/handlers/register.py b/synapse/handlers/register.py index b0862067e1..5883b9111e 100644 --- a/synapse/handlers/register.py +++ b/synapse/handlers/register.py @@ -358,6 +358,59 @@ class RegistrationHandler(BaseHandler): ) defer.returnValue(data) + @defer.inlineCallbacks + def get_or_create_user(self, localpart, displayname, duration_seconds): + """Creates a new user or returns an access token for an existing one + + Args: + localpart : The local part of the user ID to register. If None, + one will be randomly generated. + Returns: + A tuple of (user_id, access_token). + Raises: + RegistrationError if there was a problem registering. + """ + yield run_on_reactor() + + if localpart is None: + raise SynapseError(400, "Request must include user id") + + need_register = True + + try: + yield self.check_username(localpart) + except SynapseError as e: + if e.errcode == Codes.USER_IN_USE: + need_register = False + else: + raise + + user = UserID(localpart, self.hs.hostname) + user_id = user.to_string() + auth_handler = self.hs.get_handlers().auth_handler + token = auth_handler.generate_short_term_login_token(user_id, duration_seconds) + + if need_register: + yield self.store.register( + user_id=user_id, + token=token, + password_hash=None + ) + + yield registered_user(self.distributor, user) + else: + yield self.store.flush_user(user_id=user_id) + yield self.store.add_access_token_to_user(user_id=user_id, token=token) + + if displayname is not None: + logger.info("setting user display name: %s -> %s", user_id, displayname) + profile_handler = self.hs.get_handlers().profile_handler + yield profile_handler.set_displayname( + user, user, displayname + ) + + defer.returnValue((user_id, token)) + def auth_handler(self): return self.hs.get_handlers().auth_handler diff --git a/synapse/rest/client/v1/register.py b/synapse/rest/client/v1/register.py index c6a2ef2ccc..e3f4fbb0bb 100644 --- a/synapse/rest/client/v1/register.py +++ b/synapse/rest/client/v1/register.py @@ -355,5 +355,76 @@ class RegisterRestServlet(ClientV1RestServlet): ) +class CreateUserRestServlet(ClientV1RestServlet): + """Handles user creation via a server-to-server interface + """ + + PATTERNS = client_path_patterns("/createUser$", releases=()) + + def __init__(self, hs): + super(CreateUserRestServlet, self).__init__(hs) + self.store = hs.get_datastore() + self.direct_user_creation_max_duration = hs.config.user_creation_max_duration + + @defer.inlineCallbacks + def on_POST(self, request): + user_json = parse_json_object_from_request(request) + + if "access_token" not in request.args: + raise SynapseError(400, "Expected application service token.") + + app_service = yield self.store.get_app_service_by_token( + request.args["access_token"][0] + ) + if not app_service: + raise SynapseError(403, "Invalid application service token.") + + logger.debug("creating user: %s", user_json) + + response = yield self._do_create(user_json) + + defer.returnValue((200, response)) + + def on_OPTIONS(self, request): + return 403, {} + + @defer.inlineCallbacks + def _do_create(self, user_json): + yield run_on_reactor() + + if "localpart" not in user_json: + raise SynapseError(400, "Expected 'localpart' key.") + + if "displayname" not in user_json: + raise SynapseError(400, "Expected 'displayname' key.") + + if "duration_seconds" not in user_json: + raise SynapseError(400, "Expected 'duration_seconds' key.") + + localpart = user_json["localpart"].encode("utf-8") + displayname = user_json["displayname"].encode("utf-8") + duration_seconds = 0 + try: + duration_seconds = int(user_json["duration_seconds"]) + except ValueError: + raise SynapseError(400, "Failed to parse 'duration_seconds'") + if duration_seconds > self.direct_user_creation_max_duration: + duration_seconds = self.direct_user_creation_max_duration + + handler = self.handlers.registration_handler + user_id, token = yield handler.get_or_create_user( + localpart=localpart, + displayname=displayname, + duration_seconds=duration_seconds + ) + + defer.returnValue({ + "user_id": user_id, + "access_token": token, + "home_server": self.hs.hostname, + }) + + def register_servlets(hs, http_server): RegisterRestServlet(hs).register(http_server) + CreateUserRestServlet(hs).register(http_server) diff --git a/tests/api/test_auth.py b/tests/api/test_auth.py index 7e7b0b4b1d..ad269af0ec 100644 --- a/tests/api/test_auth.py +++ b/tests/api/test_auth.py @@ -284,12 +284,12 @@ class AuthTestCase(unittest.TestCase): macaroon.add_first_party_caveat("time < 1") # ms self.hs.clock.now = 5000 # seconds - - yield self.auth.get_user_from_macaroon(macaroon.serialize()) + self.hs.config.expire_access_token = True + # yield self.auth.get_user_from_macaroon(macaroon.serialize()) # TODO(daniel): Turn on the check that we validate expiration, when we # validate expiration (and remove the above line, which will start # throwing). - # with self.assertRaises(AuthError) as cm: - # yield self.auth.get_user_from_macaroon(macaroon.serialize()) - # self.assertEqual(401, cm.exception.code) - # self.assertIn("Invalid macaroon", cm.exception.msg) + with self.assertRaises(AuthError) as cm: + yield self.auth.get_user_from_macaroon(macaroon.serialize()) + self.assertEqual(401, cm.exception.code) + self.assertIn("Invalid macaroon", cm.exception.msg) diff --git a/tests/handlers/test_register.py b/tests/handlers/test_register.py new file mode 100644 index 0000000000..8b7be96bd9 --- /dev/null +++ b/tests/handlers/test_register.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Copyright 2015, 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.internet import defer +from .. import unittest + +from synapse.handlers.register import RegistrationHandler + +from tests.utils import setup_test_homeserver + +from mock import Mock + + +class RegistrationHandlers(object): + def __init__(self, hs): + self.registration_handler = RegistrationHandler(hs) + + +class RegistrationTestCase(unittest.TestCase): + """ Tests the RegistrationHandler. """ + + @defer.inlineCallbacks + def setUp(self): + self.mock_distributor = Mock() + self.mock_distributor.declare("registered_user") + self.mock_captcha_client = Mock() + hs = yield setup_test_homeserver( + handlers=None, + http_client=None, + expire_access_token=True) + hs.handlers = RegistrationHandlers(hs) + self.handler = hs.get_handlers().registration_handler + hs.get_handlers().profile_handler = Mock() + self.mock_handler = Mock(spec=[ + "generate_short_term_login_token", + ]) + + hs.get_handlers().auth_handler = self.mock_handler + + @defer.inlineCallbacks + def test_user_is_created_and_logged_in_if_doesnt_exist(self): + """ + Returns: + The user doess not exist in this case so it will register and log it in + """ + duration_ms = 200 + local_part = "someone" + display_name = "someone" + user_id = "@someone:test" + mock_token = self.mock_handler.generate_short_term_login_token + mock_token.return_value = 'secret' + result_user_id, result_token = yield self.handler.get_or_create_user( + local_part, display_name, duration_ms) + self.assertEquals(result_user_id, user_id) + self.assertEquals(result_token, 'secret') diff --git a/tests/rest/client/v1/test_register.py b/tests/rest/client/v1/test_register.py new file mode 100644 index 0000000000..4a898a034f --- /dev/null +++ b/tests/rest/client/v1/test_register.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +# Copyright 2015, 2016 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from synapse.rest.client.v1.register import CreateUserRestServlet +from twisted.internet import defer +from mock import Mock +from tests import unittest +import json + + +class CreateUserServletTestCase(unittest.TestCase): + + def setUp(self): + # do the dance to hook up request data to self.request_data + self.request_data = "" + self.request = Mock( + content=Mock(read=Mock(side_effect=lambda: self.request_data)), + path='/_matrix/client/api/v1/createUser' + ) + self.request.args = {} + + self.appservice = None + self.auth = Mock(get_appservice_by_req=Mock( + side_effect=lambda x: defer.succeed(self.appservice)) + ) + + self.auth_result = (False, None, None, None) + self.auth_handler = Mock( + check_auth=Mock(side_effect=lambda x, y, z: self.auth_result), + get_session_data=Mock(return_value=None) + ) + self.registration_handler = Mock() + self.identity_handler = Mock() + self.login_handler = Mock() + + # do the dance to hook it up to the hs global + self.handlers = Mock( + auth_handler=self.auth_handler, + registration_handler=self.registration_handler, + identity_handler=self.identity_handler, + login_handler=self.login_handler + ) + self.hs = Mock() + self.hs.hostname = "supergbig~testing~thing.com" + self.hs.get_auth = Mock(return_value=self.auth) + self.hs.get_handlers = Mock(return_value=self.handlers) + self.hs.config.enable_registration = True + # init the thing we're testing + self.servlet = CreateUserRestServlet(self.hs) + + @defer.inlineCallbacks + def test_POST_createuser_with_valid_user(self): + user_id = "@someone:interesting" + token = "my token" + self.request.args = { + "access_token": "i_am_an_app_service" + } + self.request_data = json.dumps({ + "localpart": "someone", + "displayname": "someone interesting", + "duration_seconds": 200 + }) + + self.registration_handler.get_or_create_user = Mock( + return_value=(user_id, token) + ) + + (code, result) = yield self.servlet.on_POST(self.request) + self.assertEquals(code, 200) + + det_data = { + "user_id": user_id, + "access_token": token, + "home_server": self.hs.hostname + } + self.assertDictContainsSubset(det_data, result) diff --git a/tests/utils.py b/tests/utils.py index c179df31ee..9d7978a642 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -49,6 +49,7 @@ def setup_test_homeserver(name="test", datastore=None, config=None, **kargs): config.event_cache_size = 1 config.enable_registration = True config.macaroon_secret_key = "not even a little secret" + config.expire_access_token = False config.server_name = "server.under.test" config.trusted_third_party_id_servers = [] config.room_invite_state_types = [] -- cgit 1.5.1