From 07d765209dea12229e70a09784e647611acabcda Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 19 Apr 2016 14:24:36 +0100 Subject: First bits of emailpusher Mostly logic of when to send an email --- synapse/push/emailpusher.py | 214 +++++++++++++++++++++ synapse/push/pusher.py | 4 +- synapse/storage/event_push_actions.py | 57 +++++- synapse/storage/events.py | 2 + synapse/storage/pusher.py | 27 +++ synapse/storage/schema/delta/31/events.sql | 16 ++ .../storage/schema/delta/31/pusher_throttle.sql | 23 +++ 7 files changed, 335 insertions(+), 8 deletions(-) create mode 100644 synapse/push/emailpusher.py create mode 100644 synapse/storage/schema/delta/31/events.sql create mode 100644 synapse/storage/schema/delta/31/pusher_throttle.sql (limited to 'synapse') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py new file mode 100644 index 0000000000..f9954df392 --- /dev/null +++ b/synapse/push/emailpusher.py @@ -0,0 +1,214 @@ +# -*- coding: utf-8 -*- +# Copyright 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, reactor + +import logging + +from synapse.util.metrics import Measure +from synapse.util.async import run_on_reactor + +logger = logging.getLogger(__name__) + +# The amount of time we always wait before ever emailing about a notification +# (to give the user a chance to respond to other push or notice the window) +DELAY_BEFORE_MAIL_MS = 2 * 60 * 1000 + +THROTTLE_START_MS = 2 * 60 * 1000 +THROTTLE_MAX_MS = (2 * 60 * 1000) * (2**11) # ~3 days + +# If no event triggers a notification for this long after the previous, +# the throttle is released. +THROTTLE_RESET_AFTER_MS = (2 * 60 * 1000) * (2**11) # ~3 days + + +class EmailPusher(object): + """ + A pusher that sends email notifications about events (approximately) + when they happen. + This shares quite a bit of code with httpusher: it would be good to + factor out the common parts + """ + def __init__(self, hs, pusherdict): + self.hs = hs + self.store = self.hs.get_datastore() + self.clock = self.hs.get_clock() + self.pusher_id = pusherdict['id'] + self.user_id = pusherdict['user_name'] + self.app_id = pusherdict['app_id'] + self.email = pusherdict['pushkey'] + self.last_stream_ordering = pusherdict['last_stream_ordering'] + self.timed_call = None + self.throttle_params = None + + # See httppusher + self.max_stream_ordering = None + + @defer.inlineCallbacks + def on_started(self): + self.throttle_params = yield self.store.get_throttle_params_by_room( + self.pusher_id + ) + yield self._process() + + @defer.inlineCallbacks + def on_new_notifications(self, min_stream_ordering, max_stream_ordering): + with Measure(self.clock, "push.on_new_notifications"): + self.max_stream_ordering = max(max_stream_ordering, self.max_stream_ordering) + yield self._process() + + @defer.inlineCallbacks + def on_timer(self): + self.timed_call = None + with Measure(self.clock, "push.on_timer"): + yield self._process() + + @defer.inlineCallbacks + def _process(self): + last_notifs = yield self.store.get_time_of_latest_push_action_by_room_for_user( + self.user_id + ) + + unprocessed = yield self.store.get_unread_push_actions_for_user_in_range( + self.user_id, self.last_stream_ordering, self.max_stream_ordering + ) + + soonest_due_at = None + + for push_action in unprocessed: + received_at = push_action['received_ts'] + if received_at is None: + received_at = 0 + notif_ready_at = received_at + DELAY_BEFORE_MAIL_MS + + room_ready_at = self.room_ready_to_notify_at( + push_action['room_id'], self.get_room_last_notif_ts( + last_notifs, push_action['room_id'] + ) + ) + + should_notify_at = max(notif_ready_at, room_ready_at) + + if should_notify_at < self.clock.time_msec(): + # one of our notifications is ready for sending, so we send + # *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.save_last_stream_ordering_and_success(max([ + ea['stream_ordering'] for ea in unprocessed + ])) + yield self.sent_notif_update_throttle( + push_action['room_id'], push_action + ) + else: + if soonest_due_at is None or should_notify_at < soonest_due_at: + soonest_due_at = should_notify_at + + if self.timed_call is not None: + self.timed_call.cancel() + self.timed_call = None + + if soonest_due_at is not None: + self.timed_call = reactor.callLater( + self.seconds_until(soonest_due_at), self.on_timer + ) + + @defer.inlineCallbacks + def save_last_stream_ordering_and_success(self, last_stream_ordering): + self.last_stream_ordering = last_stream_ordering + yield self.store.update_pusher_last_stream_ordering_and_success( + self.app_id, self.email, self.user_id, + last_stream_ordering, self.clock.time_msec() + ) + + def seconds_until(self, ts_msec): + return (ts_msec - self.clock.time_msec()) / 1000 + + def get_room_last_notif_ts(self, last_notif_by_room, room_id): + if room_id in last_notif_by_room: + return last_notif_by_room[room_id] + else: + return 0 + + def get_room_throttle_ms(self, room_id): + if room_id in self.throttle_params: + return self.throttle_params[room_id]["throttle_ms"] + else: + return 0 + + def get_room_last_sent_ts(self, room_id): + if room_id in self.throttle_params: + return self.throttle_params[room_id]["last_sent_ts"] + else: + return 0 + + def room_ready_to_notify_at(self, room_id, last_notif_time): + """ + Determines whether throttling should prevent us from sending an email + for the given room + Returns: True if we should send, False if we should not + """ + last_sent_ts = self.get_room_last_sent_ts(room_id) + throttle_ms = self.get_room_throttle_ms(room_id) + + may_send_at = last_sent_ts + throttle_ms + return may_send_at + + @defer.inlineCallbacks + def sent_notif_update_throttle(self, room_id, notified_push_action): + # We have sent a notification, so update the throttle accordingly. + # If the event that triggered the notif happened more than + # THROTTLE_RESET_AFTER_MS after the previous one that triggered a + # notif, we release the throttle. Otherwise, the throttle is increased. + time_of_previous_notifs = yield self.store.get_time_of_last_push_action_before( + notified_push_action['stream_ordering'] + ) + + time_of_this_notifs = notified_push_action['received_ts'] + + if time_of_previous_notifs is not None and time_of_this_notifs is not None: + gap = time_of_this_notifs - time_of_previous_notifs + else: + # if we don't know the arrival time of one of the notifs (it was not + # stored prior to email notification code) then assume a gap of + # zero which will just not reset the throttle + gap = 0 + + current_throttle_ms = self.get_room_throttle_ms(room_id) + + if gap > THROTTLE_RESET_AFTER_MS: + new_throttle_ms = THROTTLE_START_MS + else: + if current_throttle_ms == 0: + new_throttle_ms = THROTTLE_START_MS + else: + new_throttle_ms = min( + current_throttle_ms * 2, + THROTTLE_MAX_MS + ) + self.throttle_params[room_id] = { + "last_sent_ts": self.clock.time_msec(), + "throttle_ms": new_throttle_ms + } + yield self.store.set_throttle_params( + self.pusher_id, room_id, self.throttle_params[room_id] + ) + + @defer.inlineCallbacks + def send_notification(self, push_action): + yield run_on_reactor() + logger.error("sending notif email for user %r", self.user_id) \ No newline at end of file diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index 4960837504..f7c3021fcc 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -1,7 +1,9 @@ from httppusher import HttpPusher +from emailpusher import EmailPusher PUSHER_TYPES = { - 'http': HttpPusher + 'http': HttpPusher, + 'email': EmailPusher, } diff --git a/synapse/storage/event_push_actions.py b/synapse/storage/event_push_actions.py index 86a98b6f11..ad512b2f07 100644 --- a/synapse/storage/event_push_actions.py +++ b/synapse/storage/event_push_actions.py @@ -118,15 +118,17 @@ class EventPushActionsStore(SQLBaseStore): max_stream_ordering=None): def get_after_receipt(txn): sql = ( - "SELECT ep.event_id, ep.stream_ordering, ep.actions " + "SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions, " + "e.received_ts " "FROM event_push_actions AS ep, (" - " SELECT room_id, user_id," - " max(topological_ordering) as topological_ordering," - " max(stream_ordering) as stream_ordering" + " SELECT room_id, user_id, " + " max(topological_ordering) as topological_ordering, " + " max(stream_ordering) as stream_ordering " " FROM events" " NATURAL JOIN receipts_linearized WHERE receipt_type = 'm.read'" " GROUP BY room_id, user_id" ") AS rl " + "NATURAL JOIN events e " "WHERE" " ep.room_id = rl.room_id" " AND (" @@ -153,8 +155,10 @@ class EventPushActionsStore(SQLBaseStore): def get_no_receipt(txn): sql = ( - "SELECT ep.event_id, ep.stream_ordering, ep.actions " + "SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions, " + "e.received_ts " "FROM event_push_actions AS ep " + "JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id " "WHERE ep.room_id not in (" " SELECT room_id FROM events NATURAL JOIN receipts_linearized" " WHERE receipt_type = 'm.read' AND user_id = ? " @@ -175,11 +179,30 @@ class EventPushActionsStore(SQLBaseStore): defer.returnValue([ { "event_id": row[0], - "stream_ordering": row[1], - "actions": json.loads(row[2]), + "room_id": row[1], + "stream_ordering": row[2], + "actions": json.loads(row[3]), + "received_ts": row[4], } for row in after_read_receipt + no_read_receipt ]) + @defer.inlineCallbacks + def get_time_of_last_push_action_before(self, stream_ordering): + def f(txn): + sql = ( + "SELECT e.received_ts " + "FROM event_push_actions AS ep " + "JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id " + "WHERE ep.stream_ordering > ? " + "ORDER BY ep.stream_ordering ASC " + "LIMIT 1" + ) + txn.execute(sql, (stream_ordering,)) + return txn.fetchone() + result = yield self.runInteraction("get_time_of_last_push_action_before", f) + defer.returnValue(result[0] if result is not None else None) + + @defer.inlineCallbacks def get_latest_push_action_stream_ordering(self): def f(txn): @@ -190,6 +213,26 @@ class EventPushActionsStore(SQLBaseStore): ) defer.returnValue(result[0] or 0) + @defer.inlineCallbacks + def get_time_of_latest_push_action_by_room_for_user(self, user_id): + """ + Returns only the received_ts of the last notification in each of the + user's rooms, in a dict by room_id + """ + def f(txn): + txn.execute( + "SELECT ep.room_id, MAX(e.received_ts) " + "FROM event_push_actions AS ep " + "JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id " + "GROUP BY ep.room_id" + ) + return txn.fetchall() + result = yield self.runInteraction( + "get_time_of_latest_push_action_by_room_for_user", f + ) + + defer.returnValue({row[0]: row[1] for row in result}) + def _remove_push_actions_for_event_id_txn(self, txn, room_id, event_id): # Sad that we have to blow away the cache for the whole room here txn.call_after( diff --git a/synapse/storage/events.py b/synapse/storage/events.py index 21487724ed..dd58e001dc 100644 --- a/synapse/storage/events.py +++ b/synapse/storage/events.py @@ -55,6 +55,7 @@ class EventsStore(SQLBaseStore): def __init__(self, hs): super(EventsStore, self).__init__(hs) + self._clock = hs.get_clock() self.register_background_update_handler( self.EVENT_ORIGIN_SERVER_TS_NAME, self._background_reindex_origin_server_ts ) @@ -427,6 +428,7 @@ class EventsStore(SQLBaseStore): "outlier": event.internal_metadata.is_outlier(), "content": encode_json(event.content).decode("UTF-8"), "origin_server_ts": int(event.origin_server_ts), + "received_ts": self._clock.time_msec(), } for event, _ in events_and_contexts ], diff --git a/synapse/storage/pusher.py b/synapse/storage/pusher.py index e5755c0aea..caef9b59a5 100644 --- a/synapse/storage/pusher.py +++ b/synapse/storage/pusher.py @@ -230,3 +230,30 @@ class PusherStore(SQLBaseStore): {'failing_since': failing_since}, desc="update_pusher_failing_since", ) + + @defer.inlineCallbacks + def get_throttle_params_by_room(self, pusher_id): + res = yield self._simple_select_list( + "pusher_throttle", + {"pusher": pusher_id}, + ["room_id", "last_sent_ts", "throttle_ms"], + desc="get_throttle_params_by_room" + ) + + params_by_room = {} + for row in res: + params_by_room[row["room_id"]] = { + "last_sent_ts": row["last_sent_ts"], + "throttle_ms": row["throttle_ms"] + } + + defer.returnValue(params_by_room) + + @defer.inlineCallbacks + def set_throttle_params(self, pusher_id, room_id, params): + yield self._simple_upsert( + "pusher_throttle", + {"pusher": pusher_id, "room_id": room_id}, + params, + desc="set_throttle_params" + ) \ No newline at end of file diff --git a/synapse/storage/schema/delta/31/events.sql b/synapse/storage/schema/delta/31/events.sql new file mode 100644 index 0000000000..1dd0f9e170 --- /dev/null +++ b/synapse/storage/schema/delta/31/events.sql @@ -0,0 +1,16 @@ +/* Copyright 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. + */ + +ALTER TABLE events ADD COLUMN received_ts BIGINT; diff --git a/synapse/storage/schema/delta/31/pusher_throttle.sql b/synapse/storage/schema/delta/31/pusher_throttle.sql new file mode 100644 index 0000000000..d86d30c13c --- /dev/null +++ b/synapse/storage/schema/delta/31/pusher_throttle.sql @@ -0,0 +1,23 @@ +/* Copyright 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. + */ + + +CREATE TABLE pusher_throttle( + pusher BIGINT NOT NULL, + room_id TEXT NOT NULL, + last_sent_ts BIGINT, + throttle_ms BIGINT, + PRIMARY KEY (pusher, room_id) +); -- cgit 1.5.1 From e2a01455af8dbab26b4a005d847f468a51fea6c3 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 19 Apr 2016 14:52:58 +0100 Subject: Add single instance & logging stuff Copy the stuff over from http pusher that prevents multiple instances of process running at once and sets up logging and measure blocks. --- synapse/push/emailpusher.py | 47 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 8 deletions(-) (limited to 'synapse') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index f9954df392..74e3a70562 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -19,6 +19,7 @@ import logging from synapse.util.metrics import Measure from synapse.util.async import run_on_reactor +from synapse.util.logcontext import LoggingContext logger = logging.getLogger(__name__) @@ -56,6 +57,8 @@ class EmailPusher(object): # See httppusher self.max_stream_ordering = None + self.processing = False + @defer.inlineCallbacks def on_started(self): self.throttle_params = yield self.store.get_throttle_params_by_room( @@ -63,20 +66,48 @@ class EmailPusher(object): ) yield self._process() + def on_stop(self): + if self.timed_call: + self.timed_call.cancel() + @defer.inlineCallbacks def on_new_notifications(self, min_stream_ordering, max_stream_ordering): - with Measure(self.clock, "push.on_new_notifications"): - self.max_stream_ordering = max(max_stream_ordering, self.max_stream_ordering) - yield self._process() + self.max_stream_ordering = max(max_stream_ordering, self.max_stream_ordering) + yield self._process() @defer.inlineCallbacks def on_timer(self): self.timed_call = None - with Measure(self.clock, "push.on_timer"): - yield self._process() + yield self._process() @defer.inlineCallbacks def _process(self): + if self.processing: + return + + with LoggingContext("emailpush._process"): + with Measure(self.clock, "emailpush._process"): + try: + self.processing = True + # if the max ordering changes while we're running _unsafe_process, + # call it again, and so on until we've caught up. + while True: + starting_max_ordering = self.max_stream_ordering + try: + yield self._unsafe_process() + except: + logger.exception("Exception processing notifs") + if self.max_stream_ordering == starting_max_ordering: + break + finally: + self.processing = False + + def _unsafe_process(self): + """ + Main logic of the push loop without the wrapper function that sets + up logging, measures and guards against multiple instances of it + being run. + """ last_notifs = yield self.store.get_time_of_latest_push_action_by_room_for_user( self.user_id ) @@ -118,9 +149,9 @@ class EmailPusher(object): if soonest_due_at is None or should_notify_at < soonest_due_at: soonest_due_at = should_notify_at - if self.timed_call is not None: - self.timed_call.cancel() - self.timed_call = None + if self.timed_call is not None: + self.timed_call.cancel() + self.timed_call = None if soonest_due_at is not None: self.timed_call = reactor.callLater( -- cgit 1.5.1 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 +- synapse/push/emailpusher.py | 32 +++++++++++++----- synapse/push/mailer.py | 48 +++++++++++++++++++++++++++ synapse/storage/event_push_actions.py | 1 - synapse/storage/pusher.py | 2 +- 6 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 synapse/config/emailconfig.py create mode 100644 synapse/push/mailer.py (limited to 'synapse') 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 diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 74e3a70562..820c8f8467 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -18,9 +18,10 @@ from twisted.internet import defer, reactor import logging from synapse.util.metrics import Measure -from synapse.util.async import run_on_reactor from synapse.util.logcontext import LoggingContext +from mailer import Mailer + logger = logging.getLogger(__name__) # The amount of time we always wait before ever emailing about a notification @@ -28,11 +29,11 @@ logger = logging.getLogger(__name__) DELAY_BEFORE_MAIL_MS = 2 * 60 * 1000 THROTTLE_START_MS = 2 * 60 * 1000 -THROTTLE_MAX_MS = (2 * 60 * 1000) * (2**11) # ~3 days +THROTTLE_MAX_MS = (2 * 60 * 1000) * (2 ** 11) # ~3 days # If no event triggers a notification for this long after the previous, # the throttle is released. -THROTTLE_RESET_AFTER_MS = (2 * 60 * 1000) * (2**11) # ~3 days +THROTTLE_RESET_AFTER_MS = (2 * 60 * 1000) * (2 ** 11) # ~3 days class EmailPusher(object): @@ -59,12 +60,22 @@ 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, + ) + else: + self.mailer = None + @defer.inlineCallbacks def on_started(self): - self.throttle_params = yield self.store.get_throttle_params_by_room( - self.pusher_id - ) - yield self._process() + if self.mailer is not None: + self.throttle_params = yield self.store.get_throttle_params_by_room( + self.pusher_id + ) + yield self._process() def on_stop(self): if self.timed_call: @@ -102,6 +113,7 @@ class EmailPusher(object): finally: self.processing = False + @defer.inlineCallbacks def _unsafe_process(self): """ Main logic of the push loop without the wrapper function that sets @@ -241,5 +253,7 @@ class EmailPusher(object): @defer.inlineCallbacks def send_notification(self, push_action): - yield run_on_reactor() - logger.error("sending notif email for user %r", self.user_id) \ No newline at end of file + logger.info("Sending notif email for user %r", self.user_id) + yield self.mailer.send_notification_mail( + self.user_id, self.email, push_action + ) diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py new file mode 100644 index 0000000000..93d3866ec7 --- /dev/null +++ b/synapse/push/mailer.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Copyright 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 + +import smtplib +import email.utils +import email.mime.multipart +from email.mime.text import MIMEText + + +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 + + @defer.inlineCallbacks + def send_notification_mail(self, user_id, email_address, push_action): + raw_from = email.utils.parseaddr(self.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!" + + text_part = MIMEText(plainText, "plain") + text_part['Subject'] = "New Matrix Notifications" + text_part['From'] = self.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() \ No newline at end of file diff --git a/synapse/storage/event_push_actions.py b/synapse/storage/event_push_actions.py index ad512b2f07..f2af8bdb36 100644 --- a/synapse/storage/event_push_actions.py +++ b/synapse/storage/event_push_actions.py @@ -202,7 +202,6 @@ class EventPushActionsStore(SQLBaseStore): result = yield self.runInteraction("get_time_of_last_push_action_before", f) defer.returnValue(result[0] if result is not None else None) - @defer.inlineCallbacks def get_latest_push_action_stream_ordering(self): def f(txn): diff --git a/synapse/storage/pusher.py b/synapse/storage/pusher.py index caef9b59a5..5fb47d418a 100644 --- a/synapse/storage/pusher.py +++ b/synapse/storage/pusher.py @@ -256,4 +256,4 @@ class PusherStore(SQLBaseStore): {"pusher": pusher_id, "room_id": room_id}, params, desc="set_throttle_params" - ) \ No newline at end of file + ) -- cgit 1.5.1 From 05adc6c2de7def8058d97e9644dddca639886322 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 20 Apr 2016 13:02:45 +0100 Subject: more pep8 --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 93d3866ec7..97cba2ec2b 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -45,4 +45,4 @@ class Mailer(object): smtp = smtplib.SMTP(self.smtp_host, self.smtp_port) smtp.sendmail(raw_from, raw_to, text_part.as_string()) - smtp.quit() \ No newline at end of file + smtp.quit() -- 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') 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 c10ed26c303741fe0e43f11e2fbeeb148f466b17 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 21 Apr 2016 19:19:07 +0100 Subject: Flesh out email templating Mostly WIP porting the room name calculation logic from the web client so our room names in the email mirror the clients. --- synapse/push/emailpusher.py | 7 ++ synapse/push/mailer.py | 61 +++++++++++++++++- synapse/python_dependencies.py | 2 +- synapse/util/room_name.py | 142 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 synapse/util/room_name.py (limited to 'synapse') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 4e21221fb7..7c810029fa 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -82,6 +82,13 @@ class EmailPusher(object): self.max_stream_ordering = max(max_stream_ordering, self.max_stream_ordering) yield self._process() + @defer.inlineCallbacks + def on_new_receipts(self, min_stream_id, max_stream_id): + # We could wake up and cancel the timer but there tend to be quite a + # lot of read receipts so it's probably less work to just let the + # timer fire + return defer.succeed(None) + @defer.inlineCallbacks def on_timer(self): self.timed_call = None diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 0f20d43f75..e68d701ffd 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -14,18 +14,23 @@ # limitations under the License. from twisted.internet import defer - from twisted.mail.smtp import sendmail + import email.utils import email.mime.multipart from email.mime.text import MIMEText +from synapse.util.async import concurrently_execute +from synapse.util.room_name import calculate_room_name + import jinja2 class Mailer(object): def __init__(self, hs): self.hs = hs + self.store = self.hs.get_datastore() + self.state_handler = self.hs.get_state_handler() 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) @@ -38,9 +43,41 @@ class Mailer(object): if raw_to == '': raise RuntimeError("Invalid 'to' address") - plainText = self.notif_template.render() + rooms_in_order = deduped_ordered_list( + [pa['room_id'] for pa in push_actions] + ) + + notifs_by_room = {} + for pa in push_actions: + notifs_by_room.setdefault(pa["room_id"], []).append(pa) + + # collect the current state for all the rooms in which we have + # notifications + state_by_room = {} + + @defer.inlineCallbacks + def _fetch_room_state(room_id): + room_state = yield self.state_handler.get_current_state(room_id) + state_by_room[room_id] = room_state + + # Run at most 3 of these at once: sync does 10 at a time but email + # notifs are much realtime than sync so we can afford to wait a bit. + yield concurrently_execute(_fetch_room_state, rooms_in_order, 3) - text_part = MIMEText(plainText, "plain") + rooms = [ + self.get_room_vars( + r, user_id, notifs_by_room[r], state_by_room[r] + ) for r in rooms_in_order + ] + + template_vars = { + "unsubscribe_link": self.make_unsubscribe_link(), + "rooms": rooms, + } + + plainText = self.notif_template.render(**template_vars) + + text_part = MIMEText(plainText, "html") text_part['Subject'] = "New Matrix Notifications" text_part['From'] = self.hs.config.email_notif_from text_part['To'] = email_address @@ -50,3 +87,21 @@ class Mailer(object): raw_from, raw_to, text_part.as_string(), 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 + + def make_unsubscribe_link(self): + return "https://vector.im/#/settings" # XXX: matrix.to + + +def deduped_ordered_list(l): + seen = set() + ret = [] + for item in l: + if item not in seen: + seen.add(item) + ret.append(item) + return ret \ No newline at end of file diff --git a/synapse/python_dependencies.py b/synapse/python_dependencies.py index a065c78b4d..16524dbdcd 100644 --- a/synapse/python_dependencies.py +++ b/synapse/python_dependencies.py @@ -46,7 +46,7 @@ CONDITIONAL_REQUIREMENTS = { "netaddr>=0.7.18": ["netaddr"], }, "email.enable_notifs": { - "Jinja2": ["Jinja2"], + "Jinja2>=2.8": ["Jinja2>=2.8"], }, } diff --git a/synapse/util/room_name.py b/synapse/util/room_name.py new file mode 100644 index 0000000000..7e49b92bb4 --- /dev/null +++ b/synapse/util/room_name.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# Copyright 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. + +import re + +# intentionally looser than what aliases we allow to be registered since +# other HSes may allow aliases that we would not +ALIAS_RE = re.compile(r"^#.*:.+$") + +ALL_ALONE = "Empty Room" + + +def calculate_room_name(room_state, user_id): + # does it have a name? + if ("m.room.name", "") in room_state: + m_room_name = room_state[("m.room.name", "")] + if m_room_name.content and m_room_name.content["name"]: + return m_room_name.content["name"] + + # does it have a caononical alias? + if ("m.room.canonical_alias", "") in room_state: + canon_alias = room_state[("m.room.canonical_alias", "")] + if ( + canon_alias.content and canon_alias.content["alias"] and + looks_like_an_alias(canon_alias.content["alias"]) + ): + return canon_alias.content["alias"] + + # at this point we're going to need to search the state by all state keys + # for an event type, so rearrange the data structure + room_state_bytype = state_as_two_level_dict(room_state) + + # right then, any aliases at all? + if "m.room.aliases" in room_state_bytype: + m_room_aliases = room_state_bytype["m.room.aliases"] + if len(m_room_aliases.values()) > 0: + first_alias_event = m_room_aliases.values()[0] + if first_alias_event.content and first_alias_event.content["aliases"]: + the_aliases = first_alias_event.content["aliases"] + if len(the_aliases) > 0 and looks_like_an_alias(the_aliases[0]): + return the_aliases[0] + + my_member_event = None + if ("m.room.member", user_id) in room_state: + my_member_event = room_state[("m.room.member", user_id)] + + if ( + my_member_event is not None and + my_member_event.content['membership'] == "invite" + ): + if ("m.room.member", my_member_event.sender) in room_state: + inviter_member_event = room_state[("m.room.member", my_member_event.sender)] + return "Invite from %s" % (name_from_member_event(inviter_member_event),) + else: + return "Room Invite" + + # we're going to have to generate a name based on who's in the room, + # so find out who is in the room that isn't the user. + if "m.room.member" in room_state_bytype: + all_members = [ + ev for ev in room_state_bytype["m.room.member"].values() + if ev.membership == "join" or ev.membership == "invite" + ] + other_members = [m for m in all_members if m.sender != user_id] + else: + other_members = [] + all_members = [] + + if len(other_members) == 0: + if len(all_members) == 1: + # self-chat, peeked room with 1 participant, + # or inbound invite, or outbound 3PID invite. + if all_members[0].sender == user_id: + if "m.room.third_party_invite" in room_state_bytype: + third_party_invites = room_state_bytype["m.room.third_party_invite"] + if len(third_party_invites) > 0: + # technically third party invite events are not member + # events, but they are close enough + return "Inviting %s" ( + descriptor_from_member_events(third_party_invites) + ) + else: + return ALL_ALONE + else: + return name_from_member_event(all_members[0]) + else: + return ALL_ALONE + else: + return descriptor_from_member_events(other_members) + + +def state_as_two_level_dict(state): + ret = {} + for k, v in state.items(): + ret.setdefault(k[0], {})[k[1]] = v + return ret + + +def looks_like_an_alias(string): + return ALIAS_RE.match(string) is not None + + +def descriptor_from_member_events(member_events): + # else if (otherMembers.length === 1) { + # return otherMembers[0].name; + # } + # else if (otherMembers.length === 2) { + # return ( + # otherMembers[0].name + " and " + otherMembers[1].name + # ); + # } + # else { + # return ( + # otherMembers[0].name + " and " + (otherMembers.length - 1) + " others" + # ); + # } + if len(member_events) == 0: + return "nobody" + elif len(member_events) == 1: + return name_from_member_event(member_events[0]) + return "all the people, so many people. They all go hand in hand, hand in hand in their park life." + + +def name_from_member_event(member_event): + if ( + member_event.content and "displayname" in member_event.content and + member_event.content["displayname"] + ): + return member_event.content["displayname"] + return member_event.sender \ No newline at end of file -- cgit 1.5.1 From c553797c4f0b772cfa9c9370d0789bf32d82e6c5 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Apr 2016 17:27:54 +0100 Subject: No inlineCallbacks necessary on this --- synapse/push/emailpusher.py | 1 - 1 file changed, 1 deletion(-) (limited to 'synapse') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 7c810029fa..dcbee4c3fe 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -82,7 +82,6 @@ class EmailPusher(object): self.max_stream_ordering = max(max_stream_ordering, self.max_stream_ordering) yield self._process() - @defer.inlineCallbacks def on_new_receipts(self, min_stream_id, max_stream_id): # We could wake up and cancel the timer but there tend to be quite a # lot of read receipts so it's probably less work to just let the -- cgit 1.5.1 From e8701e64b9ce52a377dba7091017e5d2e116ecdf Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Apr 2016 17:28:42 +0100 Subject: Implement group-of-people names --- synapse/util/room_name.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) (limited to 'synapse') diff --git a/synapse/util/room_name.py b/synapse/util/room_name.py index 7e49b92bb4..30b7291369 100644 --- a/synapse/util/room_name.py +++ b/synapse/util/room_name.py @@ -71,9 +71,9 @@ def calculate_room_name(room_state, user_id): if "m.room.member" in room_state_bytype: all_members = [ ev for ev in room_state_bytype["m.room.member"].values() - if ev.membership == "join" or ev.membership == "invite" + if ev.content['membership'] == "join" or ev.content['membership'] == "invite" ] - other_members = [m for m in all_members if m.sender != user_id] + other_members = [m for m in all_members if m.state_key != user_id] else: other_members = [] all_members = [] @@ -113,30 +113,27 @@ def looks_like_an_alias(string): def descriptor_from_member_events(member_events): - # else if (otherMembers.length === 1) { - # return otherMembers[0].name; - # } - # else if (otherMembers.length === 2) { - # return ( - # otherMembers[0].name + " and " + otherMembers[1].name - # ); - # } - # else { - # return ( - # otherMembers[0].name + " and " + (otherMembers.length - 1) + " others" - # ); - # } if len(member_events) == 0: return "nobody" elif len(member_events) == 1: return name_from_member_event(member_events[0]) - return "all the people, so many people. They all go hand in hand, hand in hand in their park life." + elif len(member_events) == 2: + return "%s and %s" % ( + name_from_member_event(member_events[0]), + name_from_member_event(member_events[1]), + ) + else: + return "%s and %d others" % ( + name_from_member_event(member_events[0]), + len(member_events) - 1, + ) def name_from_member_event(member_event): + # XXX: Need to look in invite state for invite display names. if ( member_event.content and "displayname" in member_event.content and member_event.content["displayname"] ): return member_event.content["displayname"] - return member_event.sender \ No newline at end of file + return member_event.state_key \ No newline at end of file -- cgit 1.5.1 From 83bf65297a624a205ce13e27f0f7c887b7db48af Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Apr 2016 18:31:47 +0100 Subject: Mime part is binary so encode it first. Doesn't get character enocind right yet but makes it not error. --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index e68d701ffd..f679718b02 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -77,7 +77,7 @@ class Mailer(object): plainText = self.notif_template.render(**template_vars) - text_part = MIMEText(plainText, "html") + text_part = MIMEText(plainText.encode('utf8'), "html") text_part['Subject'] = "New Matrix Notifications" text_part['From'] = self.hs.config.email_notif_from text_part['To'] = email_address -- cgit 1.5.1 From c5b3c6e1010ce55eda27b35f008819867c21bc51 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Apr 2016 18:33:36 +0100 Subject: Sort member events So names of people in a room are given in order --- synapse/util/room_name.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'synapse') diff --git a/synapse/util/room_name.py b/synapse/util/room_name.py index 30b7291369..30ef77b9f8 100644 --- a/synapse/util/room_name.py +++ b/synapse/util/room_name.py @@ -73,6 +73,10 @@ def calculate_room_name(room_state, user_id): ev for ev in room_state_bytype["m.room.member"].values() if ev.content['membership'] == "join" or ev.content['membership'] == "invite" ] + # Sort the member events oldest-first so the we name people in the + # order the joined (it should at least be deterministic rather than + # dictionary iteration order) + all_members.sort(key=lambda e: e.origin_server_ts) other_members = [m for m in all_members if m.state_key != user_id] else: other_members = [] -- cgit 1.5.1 From bd0f9c2065a0f766786493649893489b8e0e3d38 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Apr 2016 18:42:00 +0100 Subject: Actually do UTF8 correctly --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index f679718b02..9212d36b84 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -77,7 +77,7 @@ class Mailer(object): plainText = self.notif_template.render(**template_vars) - text_part = MIMEText(plainText.encode('utf8'), "html") + 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 -- cgit 1.5.1 From 05e49ffbdf83ec3f910e1f8dbbe23aa4da297986 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 22 Apr 2016 18:44:17 +0100 Subject: No we don't: it's just the display name --- synapse/util/room_name.py | 1 - 1 file changed, 1 deletion(-) (limited to 'synapse') diff --git a/synapse/util/room_name.py b/synapse/util/room_name.py index 30ef77b9f8..d85ccaea55 100644 --- a/synapse/util/room_name.py +++ b/synapse/util/room_name.py @@ -134,7 +134,6 @@ def descriptor_from_member_events(member_events): def name_from_member_event(member_event): - # XXX: Need to look in invite state for invite display names. if ( member_event.content and "displayname" in member_event.content and member_event.content["displayname"] -- cgit 1.5.1 From 290f125a13c3c9f5cb772909248d4e482dcfb871 Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 25 Apr 2016 14:42:59 +0100 Subject: Typo --- synapse/util/room_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/util/room_name.py b/synapse/util/room_name.py index d85ccaea55..f55ef293b6 100644 --- a/synapse/util/room_name.py +++ b/synapse/util/room_name.py @@ -29,7 +29,7 @@ def calculate_room_name(room_state, user_id): if m_room_name.content and m_room_name.content["name"]: return m_room_name.content["name"] - # does it have a caononical alias? + # does it have a canonical alias? if ("m.room.canonical_alias", "") in room_state: canon_alias = room_state[("m.room.canonical_alias", "")] if ( -- cgit 1.5.1 From 7b4715bad704231b51c6d0462cfd19ed32df5e0b Mon Sep 17 00:00:00 2001 From: David Baker Date: Mon, 25 Apr 2016 18:27:04 +0100 Subject: More variable calculation for email notifs Include name of the person we're sending to and add summary text at the top giving an overview of what's happened. --- res/templates/notif.html | 3 +- synapse/push/mailer.py | 57 ++++++++++++++- synapse/util/presentable_names.py | 145 ++++++++++++++++++++++++++++++++++++++ synapse/util/room_name.py | 142 ------------------------------------- 4 files changed, 202 insertions(+), 145 deletions(-) create mode 100644 synapse/util/presentable_names.py delete mode 100644 synapse/util/room_name.py (limited to 'synapse') diff --git a/res/templates/notif.html b/res/templates/notif.html index 648ff034b3..aee52ec8c9 100644 --- a/res/templates/notif.html +++ b/res/templates/notif.html @@ -1,7 +1,8 @@ -

{{ summaryText }}

+
Hi {{ user_display_name }},
+
{{ summary_text }}
{% for room in rooms %} {% include 'room.html' with context %} diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 9212d36b84..9e2297a03b 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -21,11 +21,19 @@ import email.mime.multipart from email.mime.text import MIMEText from synapse.util.async import concurrently_execute -from synapse.util.room_name import calculate_room_name +from synapse.util.presentable_names import calculate_room_name, name_from_member_event +from synapse.types import UserID +from synapse.api.errors import StoreError import jinja2 +MESSAGE_FROM_PERSON_IN_ROOM = "You have a message from %s in the %s room" +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" + + class Mailer(object): def __init__(self, hs): self.hs = hs @@ -55,6 +63,13 @@ class Mailer(object): # notifications state_by_room = {} + try: + user_display_name = yield self.store.get_profile_displayname( + UserID.from_string(user_id).localpart + ) + except StoreError: + user_display_name = user_id + @defer.inlineCallbacks def _fetch_room_state(room_id): room_state = yield self.state_handler.get_current_state(room_id) @@ -70,8 +85,14 @@ class Mailer(object): ) for r in rooms_in_order ] + summary_text = yield self.make_summary_text( + notifs_by_room, state_by_room, user_id + ) + template_vars = { + "user_display_name": user_display_name, "unsubscribe_link": self.make_unsubscribe_link(), + "summary_text": summary_text, "rooms": rooms, } @@ -93,6 +114,38 @@ class Mailer(object): room_vars['title'] = calculate_room_name(room_state, user_id) return room_vars + @defer.inlineCallbacks + def make_summary_text(self, notifs_by_room, state_by_room, user_id): + if len(notifs_by_room) == 1: + room_id = notifs_by_room.keys()[0] + sender_name = None + if len(notifs_by_room[room_id]) == 1: + # If the room has some kind of name, use it, but we don't + # want the generated-from-names one here otherwise we'll + # end up with, "new message from Bob in the Bob room" + 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"] + ) + 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) + ) + elif sender_name is not None: + defer.returnValue(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,)) + else: + defer.returnValue(MESSAGES_IN_ROOMS) + + defer.returnValue("Some thing have occurred in some rooms") + def make_unsubscribe_link(self): return "https://vector.im/#/settings" # XXX: matrix.to @@ -104,4 +157,4 @@ def deduped_ordered_list(l): if item not in seen: seen.add(item) ret.append(item) - return ret \ No newline at end of file + return ret diff --git a/synapse/util/presentable_names.py b/synapse/util/presentable_names.py new file mode 100644 index 0000000000..2ae01e453d --- /dev/null +++ b/synapse/util/presentable_names.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# Copyright 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. + +import re + +# intentionally looser than what aliases we allow to be registered since +# other HSes may allow aliases that we would not +ALIAS_RE = re.compile(r"^#.*:.+$") + +ALL_ALONE = "Empty Room" + + +def calculate_room_name(room_state, user_id, fallback_to_members=True): + # does it have a name? + if ("m.room.name", "") in room_state: + m_room_name = room_state[("m.room.name", "")] + if m_room_name.content and m_room_name.content["name"]: + return m_room_name.content["name"] + + # does it have a canonical alias? + if ("m.room.canonical_alias", "") in room_state: + canon_alias = room_state[("m.room.canonical_alias", "")] + if ( + canon_alias.content and canon_alias.content["alias"] and + _looks_like_an_alias(canon_alias.content["alias"]) + ): + return canon_alias.content["alias"] + + # at this point we're going to need to search the state by all state keys + # for an event type, so rearrange the data structure + room_state_bytype = _state_as_two_level_dict(room_state) + + # right then, any aliases at all? + if "m.room.aliases" in room_state_bytype: + m_room_aliases = room_state_bytype["m.room.aliases"] + if len(m_room_aliases.values()) > 0: + first_alias_event = m_room_aliases.values()[0] + if first_alias_event.content and first_alias_event.content["aliases"]: + the_aliases = first_alias_event.content["aliases"] + if len(the_aliases) > 0 and _looks_like_an_alias(the_aliases[0]): + return the_aliases[0] + + my_member_event = None + if ("m.room.member", user_id) in room_state: + my_member_event = room_state[("m.room.member", user_id)] + + if ( + my_member_event is not None and + my_member_event.content['membership'] == "invite" + ): + if ("m.room.member", my_member_event.sender) in room_state: + inviter_member_event = room_state[("m.room.member", my_member_event.sender)] + return "Invite from %s" % (name_from_member_event(inviter_member_event),) + else: + return "Room Invite" + + if not fallback_to_members: + return None + + # we're going to have to generate a name based on who's in the room, + # so find out who is in the room that isn't the user. + if "m.room.member" in room_state_bytype: + all_members = [ + ev for ev in room_state_bytype["m.room.member"].values() + if ev.content['membership'] == "join" or ev.content['membership'] == "invite" + ] + # Sort the member events oldest-first so the we name people in the + # order the joined (it should at least be deterministic rather than + # dictionary iteration order) + all_members.sort(key=lambda e: e.origin_server_ts) + other_members = [m for m in all_members if m.state_key != user_id] + else: + other_members = [] + all_members = [] + + if len(other_members) == 0: + if len(all_members) == 1: + # self-chat, peeked room with 1 participant, + # or inbound invite, or outbound 3PID invite. + if all_members[0].sender == user_id: + if "m.room.third_party_invite" in room_state_bytype: + third_party_invites = room_state_bytype["m.room.third_party_invite"] + if len(third_party_invites) > 0: + # technically third party invite events are not member + # events, but they are close enough + return "Inviting %s" ( + descriptor_from_member_events(third_party_invites) + ) + else: + return ALL_ALONE + else: + return name_from_member_event(all_members[0]) + else: + return ALL_ALONE + else: + return descriptor_from_member_events(other_members) + + +def descriptor_from_member_events(member_events): + if len(member_events) == 0: + return "nobody" + elif len(member_events) == 1: + return name_from_member_event(member_events[0]) + elif len(member_events) == 2: + return "%s and %s" % ( + name_from_member_event(member_events[0]), + name_from_member_event(member_events[1]), + ) + else: + return "%s and %d others" % ( + name_from_member_event(member_events[0]), + len(member_events) - 1, + ) + + +def name_from_member_event(member_event): + if ( + member_event.content and "displayname" in member_event.content and + member_event.content["displayname"] + ): + return member_event.content["displayname"] + return member_event.state_key + + +def _state_as_two_level_dict(state): + ret = {} + for k, v in state.items(): + ret.setdefault(k[0], {})[k[1]] = v + return ret + + +def _looks_like_an_alias(string): + return ALIAS_RE.match(string) is not None \ No newline at end of file diff --git a/synapse/util/room_name.py b/synapse/util/room_name.py deleted file mode 100644 index f55ef293b6..0000000000 --- a/synapse/util/room_name.py +++ /dev/null @@ -1,142 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 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. - -import re - -# intentionally looser than what aliases we allow to be registered since -# other HSes may allow aliases that we would not -ALIAS_RE = re.compile(r"^#.*:.+$") - -ALL_ALONE = "Empty Room" - - -def calculate_room_name(room_state, user_id): - # does it have a name? - if ("m.room.name", "") in room_state: - m_room_name = room_state[("m.room.name", "")] - if m_room_name.content and m_room_name.content["name"]: - return m_room_name.content["name"] - - # does it have a canonical alias? - if ("m.room.canonical_alias", "") in room_state: - canon_alias = room_state[("m.room.canonical_alias", "")] - if ( - canon_alias.content and canon_alias.content["alias"] and - looks_like_an_alias(canon_alias.content["alias"]) - ): - return canon_alias.content["alias"] - - # at this point we're going to need to search the state by all state keys - # for an event type, so rearrange the data structure - room_state_bytype = state_as_two_level_dict(room_state) - - # right then, any aliases at all? - if "m.room.aliases" in room_state_bytype: - m_room_aliases = room_state_bytype["m.room.aliases"] - if len(m_room_aliases.values()) > 0: - first_alias_event = m_room_aliases.values()[0] - if first_alias_event.content and first_alias_event.content["aliases"]: - the_aliases = first_alias_event.content["aliases"] - if len(the_aliases) > 0 and looks_like_an_alias(the_aliases[0]): - return the_aliases[0] - - my_member_event = None - if ("m.room.member", user_id) in room_state: - my_member_event = room_state[("m.room.member", user_id)] - - if ( - my_member_event is not None and - my_member_event.content['membership'] == "invite" - ): - if ("m.room.member", my_member_event.sender) in room_state: - inviter_member_event = room_state[("m.room.member", my_member_event.sender)] - return "Invite from %s" % (name_from_member_event(inviter_member_event),) - else: - return "Room Invite" - - # we're going to have to generate a name based on who's in the room, - # so find out who is in the room that isn't the user. - if "m.room.member" in room_state_bytype: - all_members = [ - ev for ev in room_state_bytype["m.room.member"].values() - if ev.content['membership'] == "join" or ev.content['membership'] == "invite" - ] - # Sort the member events oldest-first so the we name people in the - # order the joined (it should at least be deterministic rather than - # dictionary iteration order) - all_members.sort(key=lambda e: e.origin_server_ts) - other_members = [m for m in all_members if m.state_key != user_id] - else: - other_members = [] - all_members = [] - - if len(other_members) == 0: - if len(all_members) == 1: - # self-chat, peeked room with 1 participant, - # or inbound invite, or outbound 3PID invite. - if all_members[0].sender == user_id: - if "m.room.third_party_invite" in room_state_bytype: - third_party_invites = room_state_bytype["m.room.third_party_invite"] - if len(third_party_invites) > 0: - # technically third party invite events are not member - # events, but they are close enough - return "Inviting %s" ( - descriptor_from_member_events(third_party_invites) - ) - else: - return ALL_ALONE - else: - return name_from_member_event(all_members[0]) - else: - return ALL_ALONE - else: - return descriptor_from_member_events(other_members) - - -def state_as_two_level_dict(state): - ret = {} - for k, v in state.items(): - ret.setdefault(k[0], {})[k[1]] = v - return ret - - -def looks_like_an_alias(string): - return ALIAS_RE.match(string) is not None - - -def descriptor_from_member_events(member_events): - if len(member_events) == 0: - return "nobody" - elif len(member_events) == 1: - return name_from_member_event(member_events[0]) - elif len(member_events) == 2: - return "%s and %s" % ( - name_from_member_event(member_events[0]), - name_from_member_event(member_events[1]), - ) - else: - return "%s and %d others" % ( - name_from_member_event(member_events[0]), - len(member_events) - 1, - ) - - -def name_from_member_event(member_event): - if ( - member_event.content and "displayname" in member_event.content and - member_event.content["displayname"] - ): - return member_event.content["displayname"] - return member_event.state_key \ No newline at end of file -- 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') 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 8781083960325e7aae16b1745c8e90561fde3d35 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 27 Apr 2016 15:30:41 +0100 Subject: Better grammar for multiple messages in a room Say who the messages are from if there's no room name, otherwise it's a bit nonsensical --- synapse/push/emailpusher.py | 2 +- synapse/push/mailer.py | 41 ++++++++++++++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 10 deletions(-) (limited to 'synapse') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index dcbee4c3fe..6ae16e9865 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -26,7 +26,7 @@ logger = logging.getLogger(__name__) # The amount of time we always wait before ever emailing about a notification # (to give the user a chance to respond to other push or notice the window) -DELAY_BEFORE_MAIL_MS = 2 * 60 * 1000 +DELAY_BEFORE_MAIL_MS = 2000#2 * 60 * 1000 THROTTLE_START_MS = 2 * 60 * 1000 THROTTLE_MAX_MS = (2 * 60 * 1000) * (2 ** 11) # ~3 days diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index e78c26edea..272e372beb 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -21,7 +21,9 @@ import email.mime.multipart from email.mime.text import MIMEText from synapse.util.async import concurrently_execute -from synapse.util.presentable_names import calculate_room_name, name_from_member_event +from synapse.util.presentable_names import ( + calculate_room_name, name_from_member_event, descriptor_from_member_events +) from synapse.types import UserID from synapse.api.errors import StoreError @@ -34,6 +36,7 @@ import urllib MESSAGE_FROM_PERSON_IN_ROOM = "You have a message from %s in the %s room" MESSAGE_FROM_PERSON = "You have a message from %s" +MESSAGES_FROM_PERSON = "You have messages 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" @@ -209,15 +212,19 @@ class Mailer(object): def make_summary_text(self, notifs_by_room, state_by_room, notif_events, user_id): if len(notifs_by_room) == 1: + # Only one room has new stuff room_id = notifs_by_room.keys()[0] + + # If the room has some kind of name, use it, but we don't + # want the generated-from-names one here otherwise we'll + # end up with, "new message from Bob in the Bob room" + room_name = calculate_room_name( + state_by_room[room_id], user_id, fallback_to_members=False + ) + sender_name = None if len(notifs_by_room[room_id]) == 1: - # If the room has some kind of name, use it, but we don't - # want the generated-from-names one here otherwise we'll - # end up with, "new message from Bob in the Bob room" - room_name = calculate_room_name( - state_by_room[room_id], user_id, fallback_to_members=False - ) + # There is just the one notification, so give some detail 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)] @@ -227,9 +234,25 @@ class Mailer(object): elif sender_name is not None: return MESSAGE_FROM_PERSON % (sender_name,) else: - room_name = calculate_room_name(state_by_room[room_id], user_id) - return MESSAGES_IN_ROOM % (room_name,) + # 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_name,) + else: + # If the room doesn't have a name, say who the messages + # are from explicitly to avoid, "messages in the Bob room" + sender_ids = list(set([ + notif_events[n['event_id']].sender + for n in notifs_by_room[room_id] + ])) + + return MESSAGES_FROM_PERSON % ( + descriptor_from_member_events([ + state_by_room[room_id][("m.room.member", s)] for s in sender_ids + ]) + ) else: + # Stuff's happened in multiple different rooms return MESSAGES_IN_ROOMS def make_notif_link(self, notif): -- cgit 1.5.1 From 4ed1e45869cf59b517df38831a373a1a770e5917 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 27 Apr 2016 17:18:51 +0100 Subject: Make html messages work --- synapse/push/mailer.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 272e372beb..a4a0891e05 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -59,7 +59,8 @@ ALLOWED_ATTRS = { # would make sense if we did "img": ["src"], } -ALLOWED_SCHEMES = ["http", "https", "ftp", "mailto"] +# When bleach release a version with this option, we can specify schemes +#ALLOWED_SCHEMES = ["http", "https", "ftp", "mailto"] class Mailer(object): @@ -184,7 +185,15 @@ class Mailer(object): defer.returnValue(ret) def get_message_vars(self, notif, event, room_state): - msgtype = event.content["msgtype"] + if event.type != "m.room.message": + return None + if event.content["msgtype"] != "m.text": + return None + + if "format" in event.content: + msgformat = event.content["format"] + else: + msgformat = None sender_state_event = room_state[("m.room.member", event.sender)] sender_name = name_from_member_event(sender_state_event) @@ -195,7 +204,7 @@ class Mailer(object): sender_hash = string_ordinal_total(event.sender) ret = { - "msgtype": msgtype, + "format": msgformat, "is_historical": event.event_id != notif['event_id'], "ts": event.origin_server_ts, "sender_name": sender_name, @@ -203,10 +212,10 @@ class Mailer(object): "sender_hash": sender_hash, } - if msgtype == "m.text": - ret["body_text_plain"] = event.content["body"] - elif msgtype == "org.matrix.custom.html": + if msgformat == "org.matrix.custom.html": ret["body_text_html"] = safe_markup(event.content["formatted_body"]) + else: + ret["body_text_plain"] = event.content["body"] return ret @@ -263,14 +272,14 @@ class Mailer(object): def make_unsubscribe_link(self): return "https://vector.im/#/settings" # XXX: matrix.to - def mxc_to_http_filter(self, value, width, height, resizeMethod="crop"): + def mxc_to_http_filter(self, value, width, height, resize_method="crop"): if value[0:6] != "mxc://": return "" serverAndMediaId = value[6:] params = { "width": width, "height": height, - "method": resizeMethod, + "method": resize_method, } return "%s_matrix/media/v1/thumbnail/%s?%s" % ( self.hs.config.public_baseurl, @@ -279,10 +288,12 @@ class Mailer(object): ) -def safe_markup(self, raw_html): +def safe_markup(raw_html): return jinja2.Markup(bleach.linkify(bleach.clean( raw_html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS, - protocols=ALLOWED_SCHEMES, strip=True + # bleach master has this, but it isn't released yet + # protocols=ALLOWED_SCHEMES, + strip=True ))) -- cgit 1.5.1 From 424a7f48f8d5cdb97ec3567d6841cecbf65ffda2 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 27 Apr 2016 17:50:49 +0100 Subject: Run filter_events_for_client so we don't accidentally mail out events people shouldn't see --- synapse/handlers/_base.py | 2 +- synapse/handlers/message.py | 8 ++++---- synapse/handlers/room.py | 2 +- synapse/handlers/search.py | 8 ++++---- synapse/handlers/sync.py | 4 ++-- synapse/notifier.py | 2 +- synapse/push/mailer.py | 18 +++++++++++------- 7 files changed, 24 insertions(+), 20 deletions(-) (limited to 'synapse') diff --git a/synapse/handlers/_base.py b/synapse/handlers/_base.py index 13a675b208..134729069a 100644 --- a/synapse/handlers/_base.py +++ b/synapse/handlers/_base.py @@ -192,7 +192,7 @@ class BaseHandler(object): }) @defer.inlineCallbacks - def _filter_events_for_client(self, user_id, events, is_peeking=False): + def filter_events_for_client(self, user_id, events, is_peeking=False): """ Check which events a user is allowed to see diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index f51feda2f4..7d9e3cf364 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -123,7 +123,7 @@ class MessageHandler(BaseHandler): "end": next_token.to_string(), }) - events = yield self._filter_events_for_client( + events = yield self.filter_events_for_client( user_id, events, is_peeking=(member_event_id is None), @@ -483,7 +483,7 @@ class MessageHandler(BaseHandler): ] ).addErrback(unwrapFirstError) - messages = yield self._filter_events_for_client( + messages = yield self.filter_events_for_client( user_id, messages ) @@ -619,7 +619,7 @@ class MessageHandler(BaseHandler): end_token=stream_token ) - messages = yield self._filter_events_for_client( + messages = yield self.filter_events_for_client( user_id, messages, is_peeking=is_peeking ) @@ -700,7 +700,7 @@ class MessageHandler(BaseHandler): consumeErrors=True, ).addErrback(unwrapFirstError) - messages = yield self._filter_events_for_client( + messages = yield self.filter_events_for_client( user_id, messages, is_peeking=is_peeking, ) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index dd9c18df84..fdebc9c438 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -449,7 +449,7 @@ class RoomContextHandler(BaseHandler): now_token = yield self.hs.get_event_sources().get_current_token() def filter_evts(events): - return self._filter_events_for_client( + return self.filter_events_for_client( user.to_string(), events, is_peeking=is_guest) diff --git a/synapse/handlers/search.py b/synapse/handlers/search.py index 9937d8dd7f..a937e87408 100644 --- a/synapse/handlers/search.py +++ b/synapse/handlers/search.py @@ -172,7 +172,7 @@ class SearchHandler(BaseHandler): filtered_events = search_filter.filter([r["event"] for r in results]) - events = yield self._filter_events_for_client( + events = yield self.filter_events_for_client( user.to_string(), filtered_events ) @@ -223,7 +223,7 @@ class SearchHandler(BaseHandler): r["event"] for r in results ]) - events = yield self._filter_events_for_client( + events = yield self.filter_events_for_client( user.to_string(), filtered_events ) @@ -281,11 +281,11 @@ class SearchHandler(BaseHandler): event.room_id, event.event_id, before_limit, after_limit ) - res["events_before"] = yield self._filter_events_for_client( + res["events_before"] = yield self.filter_events_for_client( user.to_string(), res["events_before"] ) - res["events_after"] = yield self._filter_events_for_client( + res["events_after"] = yield self.filter_events_for_client( user.to_string(), res["events_after"] ) diff --git a/synapse/handlers/sync.py b/synapse/handlers/sync.py index 231140b655..b51bb651ec 100644 --- a/synapse/handlers/sync.py +++ b/synapse/handlers/sync.py @@ -681,7 +681,7 @@ class SyncHandler(BaseHandler): if recents is not None: recents = sync_config.filter_collection.filter_room_timeline(recents) - recents = yield self._filter_events_for_client( + recents = yield self.filter_events_for_client( sync_config.user.to_string(), recents, ) @@ -702,7 +702,7 @@ class SyncHandler(BaseHandler): loaded_recents = sync_config.filter_collection.filter_room_timeline( events ) - loaded_recents = yield self._filter_events_for_client( + loaded_recents = yield self.filter_events_for_client( sync_config.user.to_string(), loaded_recents, ) diff --git a/synapse/notifier.py b/synapse/notifier.py index 6af7a8f424..cb58dfffd4 100644 --- a/synapse/notifier.py +++ b/synapse/notifier.py @@ -399,7 +399,7 @@ class Notifier(object): if name == "room": room_member_handler = self.hs.get_handlers().room_member_handler - new_events = yield room_member_handler._filter_events_for_client( + new_events = yield room_member_handler.filter_events_for_client( user.to_string(), new_events, is_peeking=is_peeking, diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index a4a0891e05..afdf439664 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -155,13 +155,15 @@ class Mailer(object): } for n in notifs: - vars = yield self.get_notif_vars(n, notif_events[n['event_id']], room_state) + vars = yield self.get_notif_vars( + n, user_id, notif_events[n['event_id']], room_state + ) room_vars['notifs'].append(vars) defer.returnValue(room_vars) @defer.inlineCallbacks - def get_notif_vars(self, notif, notif_event, room_state): + def get_notif_vars(self, notif, user_id, notif_event, room_state): results = yield self.store.get_events_around( notif['room_id'], notif['event_id'], before_limit=CONTEXT_BEFORE, after_limit=0 @@ -173,15 +175,17 @@ class Mailer(object): "messages": [], } - for event in results['events_before']: + handler = self.hs.get_handlers().message_handler + the_events = yield handler.filter_events_for_client( + user_id, results["events_before"] + ) + the_events.append(notif_event) + + for event in the_events: 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): -- cgit 1.5.1 From 9dba1b668ce1d2ea4db1fb6bfae3df319e7c76d4 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 10:55:08 +0100 Subject: Linkify plain text messages too --- synapse/push/mailer.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index afdf439664..7ef64f8f6d 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -219,7 +219,7 @@ class Mailer(object): if msgformat == "org.matrix.custom.html": ret["body_text_html"] = safe_markup(event.content["formatted_body"]) else: - ret["body_text_plain"] = event.content["body"] + ret["body_text_html"] = safe_text(event.content["body"]) return ret @@ -301,6 +301,17 @@ def safe_markup(raw_html): ))) +def safe_text(raw_text): + """ + Process text: treat it as HTML but escape any tags (ie. just escape the + HTML) then linkify it. + """ + return jinja2.Markup(bleach.linkify(bleach.clean( + raw_text, tags=[], attributes={}, + strip=False + ))) + + def deduped_ordered_list(l): seen = set() ret = [] -- cgit 1.5.1 From ebbabc4986371c83d1d2659d10b27caad9b47951 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 11:49:36 +0100 Subject: Handle room invites in email notifs --- res/templates/room.html | 14 +++++++++----- synapse/push/mailer.py | 35 ++++++++++++++++++++++++++++++----- synapse/util/presentable_names.py | 6 +++--- 3 files changed, 42 insertions(+), 13 deletions(-) (limited to 'synapse') diff --git a/res/templates/room.html b/res/templates/room.html index f369575b98..6c68ee1fdc 100644 --- a/res/templates/room.html +++ b/res/templates/room.html @@ -13,9 +13,13 @@ {% endif %} {% endif %}
-
- {% for notif in room.notifs %} - {% include 'notif.html' with context %} - {% endfor %} -
+ {% if room.invite %} + Join the conversation. + {% else %} +
+ {% for notif in room.notifs %} + {% include 'notif.html' with context %} + {% endfor %} +
+ {% endif %} diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 7ef64f8f6d..d2cf24765a 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -39,6 +39,8 @@ MESSAGE_FROM_PERSON = "You have a message from %s" MESSAGES_FROM_PERSON = "You have messages 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" +INVITE_FROM_PERSON_TO_ROOM = "%s has invited you to join the %s room" +INVITE_FROM_PERSON = "%s has invited you to chat" CONTEXT_BEFORE = 1 @@ -148,17 +150,24 @@ class Mailer(object): @defer.inlineCallbacks def get_room_vars(self, room_id, user_id, notifs, notif_events, room_state): + my_member_event = room_state[("m.room.member", user_id)] + is_invite = my_member_event.content["membership"] == "invite" + room_vars = { "title": calculate_room_name(room_state, user_id), "hash": string_ordinal_total(room_id), # See sender avatar hash "notifs": [], + "invite": is_invite } - for n in notifs: - vars = yield self.get_notif_vars( - n, user_id, notif_events[n['event_id']], room_state - ) - room_vars['notifs'].append(vars) + if is_invite: + room_vars["link"] = self.make_room_link(room_id) + else: + for n in notifs: + vars = yield self.get_notif_vars( + n, user_id, notif_events[n['event_id']], room_state + ) + room_vars['notifs'].append(vars) defer.returnValue(room_vars) @@ -235,6 +244,18 @@ class Mailer(object): state_by_room[room_id], user_id, fallback_to_members=False ) + my_member_event = state_by_room[room_id][("m.room.member", user_id)] + if my_member_event.content["membership"] == "invite": + inviter_member_event = state_by_room[room_id][ + ("m.room.member", my_member_event.sender) + ] + inviter_name = name_from_member_event(inviter_member_event) + + if room_name is None: + return INVITE_FROM_PERSON % (inviter_name,) + else: + return INVITE_FROM_PERSON_TO_ROOM % (inviter_name, room_name) + sender_name = None if len(notifs_by_room[room_id]) == 1: # There is just the one notification, so give some detail @@ -242,6 +263,7 @@ class Mailer(object): 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: return MESSAGE_FROM_PERSON_IN_ROOM % (sender_name, room_name) elif sender_name is not None: @@ -268,6 +290,9 @@ class Mailer(object): # Stuff's happened in multiple different rooms return MESSAGES_IN_ROOMS + def make_room_link(self, room_id): + return "https://matrix.to/%s" % (room_id,) + def make_notif_link(self, notif): return "https://matrix.to/%s/%s" % ( notif['room_id'], notif['event_id'] diff --git a/synapse/util/presentable_names.py b/synapse/util/presentable_names.py index 2ae01e453d..f80a7fe58e 100644 --- a/synapse/util/presentable_names.py +++ b/synapse/util/presentable_names.py @@ -52,6 +52,9 @@ def calculate_room_name(room_state, user_id, fallback_to_members=True): if len(the_aliases) > 0 and _looks_like_an_alias(the_aliases[0]): return the_aliases[0] + if not fallback_to_members: + return None + my_member_event = None if ("m.room.member", user_id) in room_state: my_member_event = room_state[("m.room.member", user_id)] @@ -66,9 +69,6 @@ def calculate_room_name(room_state, user_id, fallback_to_members=True): else: return "Room Invite" - if not fallback_to_members: - return None - # we're going to have to generate a name based on who's in the room, # so find out who is in the room that isn't the user. if "m.room.member" in room_state_bytype: -- cgit 1.5.1 From 937c407eef78648f1c4c1afe56fdfd8598315b19 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 15:12:14 +0100 Subject: Only import email pusher if email notifs are on --- synapse/push/pusher.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'synapse') diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index f7c3021fcc..25a45af775 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -1,12 +1,14 @@ from httppusher import HttpPusher -from emailpusher import EmailPusher - -PUSHER_TYPES = { - 'http': HttpPusher, - 'email': EmailPusher, -} def create_pusher(hs, pusherdict): + PUSHER_TYPES = { + "http": HttpPusher, + } + + if hs.config.email_enable_notifs: + from emailpusher import EmailPusher + PUSHER_TYPES["email"] = EmailPusher + if pusherdict['kind'] in PUSHER_TYPES: return PUSHER_TYPES[pusherdict['kind']](hs, pusherdict) -- 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') 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 4845c7359de6c1ad1e7132653939e44ee8ff9156 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 15:55:53 +0100 Subject: Support image notifs --- res/templates/notif.html | 12 ++++++++---- synapse/push/mailer.py | 36 +++++++++++++++++++++++++----------- 2 files changed, 33 insertions(+), 15 deletions(-) (limited to 'synapse') diff --git a/res/templates/notif.html b/res/templates/notif.html index aa6ed1e061..bdff2786ff 100644 --- a/res/templates/notif.html +++ b/res/templates/notif.html @@ -17,10 +17,14 @@
{{ message.sender_name }}
{{ message.ts|format_ts("%H:%M") }}
- {% if message.format == "org.matrix.custom.html" %} - {{ message.body_text_html }} - {% else %} - {{ message.body_text_plain }} + {% if message.msgtype == "m.text" %} + {% if message.format == "org.matrix.custom.html" %} + {{ message.body_text_html }} + {% else %} + {{ message.body_text_plain }} + {% endif %} + {% elif message.msgtype == "m.image" %} + {% endif %}
diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index ae3e41b8ce..e6554dc7fd 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -200,13 +200,6 @@ class Mailer(object): def get_message_vars(self, notif, event, room_state): if event.type != "m.room.message": return None - if event.content["msgtype"] != "m.text": - return None - - if "format" in event.content: - msgformat = event.content["format"] - else: - msgformat = None sender_state_event = room_state[("m.room.member", event.sender)] sender_name = name_from_member_event(sender_state_event) @@ -217,7 +210,7 @@ class Mailer(object): sender_hash = string_ordinal_total(event.sender) ret = { - "format": msgformat, + "msgtype": event.content["msgtype"], "is_historical": event.event_id != notif['event_id'], "ts": event.origin_server_ts, "sender_name": sender_name, @@ -225,13 +218,34 @@ class Mailer(object): "sender_hash": sender_hash, } - if msgformat == "org.matrix.custom.html": - ret["body_text_html"] = safe_markup(event.content["formatted_body"]) + if event.content["msgtype"] == "m.text": + self.add_text_message_vars(ret, event) + elif event.content["msgtype"] == "m.image": + self.add_image_message_vars(ret, event) else: - ret["body_text_html"] = safe_text(event.content["body"]) + return None return ret + def add_text_message_vars(self, vars, event): + if "format" in event.content: + msgformat = event.content["format"] + else: + msgformat = None + vars["format"] = msgformat + + if msgformat == "org.matrix.custom.html": + vars["body_text_html"] = safe_markup(event.content["formatted_body"]) + else: + vars["body_text_html"] = safe_text(event.content["body"]) + + return vars + + def add_image_message_vars(self, vars, event): + vars["image_url"] = event.content["url"] + + return vars + def make_summary_text(self, notifs_by_room, state_by_room, notif_events, user_id): if len(notifs_by_room) == 1: # Only one room has new stuff -- cgit 1.5.1 From 68f8fc2f143d44b560b07e7521b28aae332e243d Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 16:59:57 +0100 Subject: Support file messages & fix plain text --- res/templates/notif.html | 8 +++----- synapse/push/mailer.py | 5 +++-- 2 files changed, 6 insertions(+), 7 deletions(-) (limited to 'synapse') diff --git a/res/templates/notif.html b/res/templates/notif.html index bdff2786ff..70f5655352 100644 --- a/res/templates/notif.html +++ b/res/templates/notif.html @@ -18,13 +18,11 @@
{{ message.ts|format_ts("%H:%M") }}
{% if message.msgtype == "m.text" %} - {% if message.format == "org.matrix.custom.html" %} - {{ message.body_text_html }} - {% else %} - {{ message.body_text_plain }} - {% endif %} + {{ message.body_text_html }} {% elif message.msgtype == "m.image" %} + {% elif message.msgtype == "m.file" %} + {{ message.body_text_plain }} {% endif %}
diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index e6554dc7fd..60a4878a3e 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -222,8 +222,9 @@ class Mailer(object): self.add_text_message_vars(ret, event) elif event.content["msgtype"] == "m.image": self.add_image_message_vars(ret, event) - else: - return None + + if "body" in event.content: + ret["body_text_plain"] = event.content["body"] return ret -- cgit 1.5.1 From cc0874cf71b2d2eef713155900b4ac5384ba1a1a Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 17:00:40 +0100 Subject: Put back real delay before mailing --- synapse/push/emailpusher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 6ae16e9865..dcbee4c3fe 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -26,7 +26,7 @@ logger = logging.getLogger(__name__) # The amount of time we always wait before ever emailing about a notification # (to give the user a chance to respond to other push or notice the window) -DELAY_BEFORE_MAIL_MS = 2000#2 * 60 * 1000 +DELAY_BEFORE_MAIL_MS = 2 * 60 * 1000 THROTTLE_START_MS = 2 * 60 * 1000 THROTTLE_MAX_MS = (2 * 60 * 1000) * (2 ** 11) # ~3 days -- cgit 1.5.1 From e800ee2f636a6aa63a9ac35388b9f982b01cd1c0 Mon Sep 17 00:00:00 2001 From: David Baker Date: Thu, 28 Apr 2016 17:28:27 +0100 Subject: May as well always include room link --- synapse/push/mailer.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 60a4878a3e..c53ae9a547 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -157,12 +157,11 @@ class Mailer(object): "title": calculate_room_name(room_state, user_id), "hash": string_ordinal_total(room_id), # See sender avatar hash "notifs": [], - "invite": is_invite + "invite": is_invite, + "link": self.make_room_link(room_id), } - if is_invite: - room_vars["link"] = self.make_room_link(room_id) - else: + if not is_invite: for n in notifs: vars = yield self.get_notif_vars( n, user_id, notif_events[n['event_id']], room_state -- cgit 1.5.1 From ec9cbe847d0d489c602309312fb947daa0e6a129 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 10:07:30 +0100 Subject: pep8 newline --- synapse/util/presentable_names.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/util/presentable_names.py b/synapse/util/presentable_names.py index f80a7fe58e..13def91776 100644 --- a/synapse/util/presentable_names.py +++ b/synapse/util/presentable_names.py @@ -142,4 +142,4 @@ def _state_as_two_level_dict(state): def _looks_like_an_alias(string): - return ALIAS_RE.match(string) is not None \ No newline at end of file + return ALIAS_RE.match(string) is not None -- cgit 1.5.1 From b2c04da8dc98ca09620dc207c95f68b2e8a52e62 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 11:43:57 +0100 Subject: Add an email pusher for new users If they registered with an email address and email notifs are enabled on the HS --- synapse/push/pusherpool.py | 1 + synapse/rest/client/v2_alpha/register.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) (limited to 'synapse') diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 6ef48d63f7..7fef2fb6f7 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -50,6 +50,7 @@ class PusherPool: # recreated, added and started: this means we have only one # code path adding pushers. pusher.create_pusher(self.hs, { + "id": None, "user_name": user_id, "kind": kind, "app_id": app_id, diff --git a/synapse/rest/client/v2_alpha/register.py b/synapse/rest/client/v2_alpha/register.py index ff8f69ddbf..883b1c1291 100644 --- a/synapse/rest/client/v2_alpha/register.py +++ b/synapse/rest/client/v2_alpha/register.py @@ -48,6 +48,7 @@ class RegisterRestServlet(RestServlet): super(RegisterRestServlet, self).__init__() self.hs = hs self.auth = hs.get_auth() + self.store = hs.get_datastore() self.auth_handler = hs.get_handlers().auth_handler self.registration_handler = hs.get_handlers().registration_handler self.identity_handler = hs.get_handlers().identity_handler @@ -214,6 +215,31 @@ class RegisterRestServlet(RestServlet): threepid['validated_at'], ) + # And we add an email pusher for them by default, but only + # 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: + # 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 + # invasive change I'd rather do separately. + user_tuple = yield self.store.get_user_by_access_token( + token + ) + + yield self.hs.get_pusherpool().add_pusher( + user_id=user_id, + access_token=user_tuple["token_id"], + kind="email", + app_id="m.email", + app_display_name="Email Notifications", + device_display_name=threepid["address"], + pushkey=threepid["address"], + lang=None, # We don't know a user's language here + data={}, + ) + if 'bind_email' in params and params['bind_email']: logger.info("bind_email specified: binding") -- 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') 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') 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') 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') 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') 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 4364ea1272a2201fc434f4cf6f0c3d13f5cbc37e Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 14:31:27 +0100 Subject: Stop processing notifs once we've sent a mail --- synapse/push/emailpusher.py | 1 + 1 file changed, 1 insertion(+) (limited to 'synapse') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index dcbee4c3fe..68f59a3faa 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -159,6 +159,7 @@ class EmailPusher(object): yield self.sent_notif_update_throttle( push_action['room_id'], push_action ) + break else: if soonest_due_at is None or should_notify_at < soonest_due_at: soonest_due_at = should_notify_at -- cgit 1.5.1 From 3facde2536f392477d59c2e03be2f2fb07e383dc Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 14:36:45 +0100 Subject: Remove rather pointless get function --- synapse/push/emailpusher.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) (limited to 'synapse') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 68f59a3faa..62e1e40326 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -139,9 +139,7 @@ class EmailPusher(object): notif_ready_at = received_at + DELAY_BEFORE_MAIL_MS room_ready_at = self.room_ready_to_notify_at( - push_action['room_id'], self.get_room_last_notif_ts( - last_notifs, push_action['room_id'] - ) + push_action['room_id'], last_notifs.get(push_action['room_id'], 0) ) should_notify_at = max(notif_ready_at, room_ready_at) @@ -184,12 +182,6 @@ class EmailPusher(object): def seconds_until(self, ts_msec): return (ts_msec - self.clock.time_msec()) / 1000 - def get_room_last_notif_ts(self, last_notif_by_room, room_id): - if room_id in last_notif_by_room: - return last_notif_by_room[room_id] - else: - return 0 - def get_room_throttle_ms(self, room_id): if room_id in self.throttle_params: return self.throttle_params[room_id]["throttle_ms"] -- cgit 1.5.1 From 311b5ce051cf979f30f6aa53051b1349326b2235 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 14:37:30 +0100 Subject: pep8 --- synapse/push/mailer.py | 1 - 1 file changed, 1 deletion(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 4fd89b3e90..0ab5168e20 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -154,7 +154,6 @@ class Mailer(object): multipart_msg.attach(text_part) multipart_msg.attach(html_part) - yield sendmail( self.hs.config.email_smtp_host, raw_from, raw_to, multipart_msg.as_string(), -- 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') 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 d3da5294e8acaba34c59bdfc62f7010363b17a18 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 19:04:40 +0100 Subject: Use named parameter format --- synapse/push/mailer.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 0ab5168e20..b193bb84b1 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -35,13 +35,13 @@ import time import urllib -MESSAGE_FROM_PERSON_IN_ROOM = "You have a message from %s in the %s room" -MESSAGE_FROM_PERSON = "You have a message from %s" -MESSAGES_FROM_PERSON = "You have messages from %s" -MESSAGES_IN_ROOM = "There are some messages for you in the %s room" +MESSAGE_FROM_PERSON_IN_ROOM = "You have a message from %(person)s in the %s room" +MESSAGE_FROM_PERSON = "You have a message from %(person)s" +MESSAGES_FROM_PERSON = "You have messages from %(person)s" +MESSAGES_IN_ROOM = "There are some messages for you in the %(room)s room" MESSAGES_IN_ROOMS = "Here are some messages you may have missed" -INVITE_FROM_PERSON_TO_ROOM = "%s has invited you to join the %s room" -INVITE_FROM_PERSON = "%s has invited you to chat" +INVITE_FROM_PERSON_TO_ROOM = "%(person)s has invited you to join the %(room)s room" +INVITE_FROM_PERSON = "%(person)s has invited you to chat" CONTEXT_BEFORE = 1 @@ -278,9 +278,11 @@ class Mailer(object): inviter_name = name_from_member_event(inviter_member_event) if room_name is None: - return INVITE_FROM_PERSON % (inviter_name,) + return INVITE_FROM_PERSON % {"person": inviter_name} else: - return INVITE_FROM_PERSON_TO_ROOM % (inviter_name, room_name) + return INVITE_FROM_PERSON_TO_ROOM % { + "person": inviter_name, "room": room_name + } sender_name = None if len(notifs_by_room[room_id]) == 1: @@ -291,14 +293,20 @@ class Mailer(object): sender_name = name_from_member_event(state_event) if sender_name is not None and room_name is not None: - return MESSAGE_FROM_PERSON_IN_ROOM % (sender_name, room_name) + return MESSAGE_FROM_PERSON_IN_ROOM % { + "person": sender_name, "room": room_name + } elif sender_name is not None: - return MESSAGE_FROM_PERSON % (sender_name,) + return MESSAGE_FROM_PERSON % { + "person": sender_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_name,) + return MESSAGES_IN_ROOM % { + "room": room_name + } else: # If the room doesn't have a name, say who the messages # are from explicitly to avoid, "messages in the Bob room" @@ -307,12 +315,12 @@ class Mailer(object): for n in notifs_by_room[room_id] ])) - return MESSAGES_FROM_PERSON % ( - descriptor_from_member_events([ + return MESSAGES_FROM_PERSON % { + "person": descriptor_from_member_events([ state_by_room[room_id][("m.room.member", s)] for s in sender_ids ]) - ) + } else: # Stuff's happened in multiple different rooms return MESSAGES_IN_ROOMS -- cgit 1.5.1 From 29c8cf8db81b89b1571b0a042d2bfed3ed278890 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 19:09:28 +0100 Subject: Avoid `vars` builtin --- synapse/push/mailer.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index b193bb84b1..95f872ccf0 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -125,10 +125,10 @@ class Mailer(object): rooms = [] for r in rooms_in_order: - vars = yield self.get_room_vars( + roomvars = yield self.get_room_vars( r, user_id, notifs_by_room[r], notif_events, state_by_room[r] ) - rooms.append(vars) + rooms.append(roomvars) summary_text = self.make_summary_text( notifs_by_room, state_by_room, notif_events, user_id @@ -175,10 +175,10 @@ class Mailer(object): if not is_invite: for n in notifs: - vars = yield self.get_notif_vars( + notifvars = yield self.get_notif_vars( n, user_id, notif_events[n['event_id']], room_state ) - room_vars['notifs'].append(vars) + room_vars['notifs'].append(notifvars) defer.returnValue(room_vars) @@ -202,9 +202,9 @@ class Mailer(object): the_events.append(notif_event) for event in the_events: - vars = self.get_message_vars(notif, event, room_state) - if vars is not None: - ret['messages'].append(vars) + messagevars = self.get_message_vars(notif, event, room_state) + if messagevars is not None: + ret['messages'].append(messagevars) defer.returnValue(ret) @@ -239,24 +239,24 @@ class Mailer(object): return ret - def add_text_message_vars(self, vars, event): + def add_text_message_vars(self, messagevars, event): if "format" in event.content: msgformat = event.content["format"] else: msgformat = None - vars["format"] = msgformat + messagevars["format"] = msgformat if msgformat == "org.matrix.custom.html": - vars["body_text_html"] = safe_markup(event.content["formatted_body"]) + messagevars["body_text_html"] = safe_markup(event.content["formatted_body"]) else: - vars["body_text_html"] = safe_text(event.content["body"]) + messagevars["body_text_html"] = safe_text(event.content["body"]) - return vars + return messagevars - def add_image_message_vars(self, vars, event): - vars["image_url"] = event.content["url"] + def add_image_message_vars(self, messagevars, event): + messagevars["image_url"] = event.content["url"] - return vars + return messagevars def make_summary_text(self, notifs_by_room, state_by_room, notif_events, user_id): if len(notifs_by_room) == 1: -- cgit 1.5.1 From e7a76b512302ee77e3afc6c17dfb2efc3b58b156 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 19:10:45 +0100 Subject: Use the constant --- synapse/push/mailer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 95f872ccf0..1e6654e2bb 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -27,6 +27,7 @@ from synapse.util.presentable_names import ( ) from synapse.types import UserID from synapse.api.errors import StoreError +from synapse.api.constants import EventTypes import jinja2 import bleach @@ -209,7 +210,7 @@ class Mailer(object): defer.returnValue(ret) def get_message_vars(self, notif, event, room_state): - if event.type != "m.room.message": + if event.type != EventTypes.Message: return None sender_state_event = room_state[("m.room.member", event.sender)] -- 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') 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 50ad8005e4b3e8054f990f34d2d7735b09dc8c19 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 19:16:15 +0100 Subject: Put spaces at start of line --- synapse/storage/event_push_actions.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) (limited to 'synapse') diff --git a/synapse/storage/event_push_actions.py b/synapse/storage/event_push_actions.py index f2af8bdb36..5b9a4ca60d 100644 --- a/synapse/storage/event_push_actions.py +++ b/synapse/storage/event_push_actions.py @@ -155,13 +155,13 @@ class EventPushActionsStore(SQLBaseStore): def get_no_receipt(txn): sql = ( - "SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions, " - "e.received_ts " - "FROM event_push_actions AS ep " - "JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id " - "WHERE ep.room_id not in (" + "SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions," + " e.received_ts" + " FROM event_push_actions AS ep" + " JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id" + " WHERE ep.room_id not in (" " SELECT room_id FROM events NATURAL JOIN receipts_linearized" - " WHERE receipt_type = 'm.read' AND user_id = ? " + " WHERE receipt_type = 'm.read' AND user_id = ?" " GROUP BY room_id" ") AND ep.user_id = ? AND ep.stream_ordering > ?" ) @@ -190,12 +190,12 @@ class EventPushActionsStore(SQLBaseStore): def get_time_of_last_push_action_before(self, stream_ordering): def f(txn): sql = ( - "SELECT e.received_ts " - "FROM event_push_actions AS ep " - "JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id " - "WHERE ep.stream_ordering > ? " - "ORDER BY ep.stream_ordering ASC " - "LIMIT 1" + "SELECT e.received_ts" + " FROM event_push_actions AS ep" + " JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id" + " WHERE ep.stream_ordering > ?" + " ORDER BY ep.stream_ordering ASC" + " LIMIT 1" ) txn.execute(sql, (stream_ordering,)) return txn.fetchone() @@ -220,10 +220,10 @@ class EventPushActionsStore(SQLBaseStore): """ def f(txn): txn.execute( - "SELECT ep.room_id, MAX(e.received_ts) " - "FROM event_push_actions AS ep " - "JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id " - "GROUP BY ep.room_id" + "SELECT ep.room_id, MAX(e.received_ts)" + " FROM event_push_actions AS ep" + " JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id" + " GROUP BY ep.room_id" ) return txn.fetchall() result = yield self.runInteraction( -- cgit 1.5.1 From 60f44c098d14754d0159c2ccc2c888e7ca970427 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 19:17:10 +0100 Subject: Remove unnecessary if --- synapse/storage/event_push_actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/storage/event_push_actions.py b/synapse/storage/event_push_actions.py index 5b9a4ca60d..c6625a7b08 100644 --- a/synapse/storage/event_push_actions.py +++ b/synapse/storage/event_push_actions.py @@ -200,7 +200,7 @@ class EventPushActionsStore(SQLBaseStore): txn.execute(sql, (stream_ordering,)) return txn.fetchone() result = yield self.runInteraction("get_time_of_last_push_action_before", f) - defer.returnValue(result[0] if result is not None else None) + defer.returnValue(result[0] if result else None) @defer.inlineCallbacks def get_latest_push_action_stream_ordering(self): -- cgit 1.5.1 From 8f99cd5996a11bb1cc50dce35542b2591426fdbc Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 19:27:03 +0100 Subject: Oops, actually specify the user id --- synapse/storage/event_push_actions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/storage/event_push_actions.py b/synapse/storage/event_push_actions.py index c6625a7b08..974ceb3389 100644 --- a/synapse/storage/event_push_actions.py +++ b/synapse/storage/event_push_actions.py @@ -223,7 +223,9 @@ class EventPushActionsStore(SQLBaseStore): "SELECT ep.room_id, MAX(e.received_ts)" " FROM event_push_actions AS ep" " JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id" - " GROUP BY ep.room_id" + " WHERE ep.user_id = ?" + " GROUP BY ep.room_id", + (user_id,) ) return txn.fetchall() result = yield self.runInteraction( -- cgit 1.5.1 From b0a1036d93eaec50839c95bf0dc621826342ea62 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 19:28:56 +0100 Subject: Use explicit join --- synapse/storage/event_push_actions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'synapse') diff --git a/synapse/storage/event_push_actions.py b/synapse/storage/event_push_actions.py index 974ceb3389..b710101b15 100644 --- a/synapse/storage/event_push_actions.py +++ b/synapse/storage/event_push_actions.py @@ -127,9 +127,9 @@ class EventPushActionsStore(SQLBaseStore): " FROM events" " NATURAL JOIN receipts_linearized WHERE receipt_type = 'm.read'" " GROUP BY room_id, user_id" - ") AS rl " - "NATURAL JOIN events e " - "WHERE" + ") AS rl" + " INNER JOIN events AS e USING (room_id, event_id)" + " WHERE" " ep.room_id = rl.room_id" " AND (" " ep.topological_ordering > rl.topological_ordering" -- cgit 1.5.1 From c7c75e87dc6b619d07d224bd4c6464313cc96840 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 19:47:35 +0100 Subject: Docstring --- synapse/util/presentable_names.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) (limited to 'synapse') diff --git a/synapse/util/presentable_names.py b/synapse/util/presentable_names.py index 13def91776..3efa8a8206 100644 --- a/synapse/util/presentable_names.py +++ b/synapse/util/presentable_names.py @@ -23,6 +23,20 @@ ALL_ALONE = "Empty Room" def calculate_room_name(room_state, user_id, fallback_to_members=True): + """ + Works out a user-facing name for the given room as per Matrix + spec recommendations. + Does not yet support internationalisation. + Args: + room_state: Dictionary of the room's state + user_id: The ID of the user to whom the room name is being presented + fallback_to_members: If False, return None instead of generating a name + based on the room's members if the room has no + title or aliases. + + Returns: + (string or None) A human readable name for the room. + """ # does it have a name? if ("m.room.name", "") in room_state: m_room_name = room_state[("m.room.name", "")] -- cgit 1.5.1 From 6b9b6a91691d543fe420e82366f641beb760cf11 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 20:02:52 +0100 Subject: Remove unused arg --- synapse/push/emailpusher.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'synapse') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 62e1e40326..8b105b85c8 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -139,7 +139,7 @@ class EmailPusher(object): notif_ready_at = received_at + DELAY_BEFORE_MAIL_MS room_ready_at = self.room_ready_to_notify_at( - push_action['room_id'], last_notifs.get(push_action['room_id'], 0) + push_action['room_id'] ) should_notify_at = max(notif_ready_at, room_ready_at) @@ -194,7 +194,7 @@ class EmailPusher(object): else: return 0 - def room_ready_to_notify_at(self, room_id, last_notif_time): + def room_ready_to_notify_at(self, room_id): """ Determines whether throttling should prevent us from sending an email for the given room -- cgit 1.5.1 From 35b7b8e4bccd8cfbe44b2ffee97cdee0a48701ba Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 20:10:34 +0100 Subject: Remove unused function --- synapse/push/emailpusher.py | 4 ---- synapse/storage/event_push_actions.py | 22 ---------------------- 2 files changed, 26 deletions(-) (limited to 'synapse') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index 8b105b85c8..c10deded06 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -122,10 +122,6 @@ class EmailPusher(object): up logging, measures and guards against multiple instances of it being run. """ - last_notifs = yield self.store.get_time_of_latest_push_action_by_room_for_user( - self.user_id - ) - unprocessed = yield self.store.get_unread_push_actions_for_user_in_range( self.user_id, self.last_stream_ordering, self.max_stream_ordering ) diff --git a/synapse/storage/event_push_actions.py b/synapse/storage/event_push_actions.py index b710101b15..85290d2a90 100644 --- a/synapse/storage/event_push_actions.py +++ b/synapse/storage/event_push_actions.py @@ -212,28 +212,6 @@ class EventPushActionsStore(SQLBaseStore): ) defer.returnValue(result[0] or 0) - @defer.inlineCallbacks - def get_time_of_latest_push_action_by_room_for_user(self, user_id): - """ - Returns only the received_ts of the last notification in each of the - user's rooms, in a dict by room_id - """ - def f(txn): - txn.execute( - "SELECT ep.room_id, MAX(e.received_ts)" - " FROM event_push_actions AS ep" - " JOIN events e ON ep.room_id = e.room_id AND ep.event_id = e.event_id" - " WHERE ep.user_id = ?" - " GROUP BY ep.room_id", - (user_id,) - ) - return txn.fetchall() - result = yield self.runInteraction( - "get_time_of_latest_push_action_by_room_for_user", f - ) - - defer.returnValue({row[0]: row[1] for row in result}) - def _remove_push_actions_for_event_id_txn(self, txn, room_id, event_id): # Sad that we have to blow away the cache for the whole room here txn.call_after( -- cgit 1.5.1 From b084e4d963f2df377dfe98b6d9645fa9183a368b Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 29 Apr 2016 20:14:55 +0100 Subject: Add constant for throttle multiplier --- synapse/push/emailpusher.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/push/emailpusher.py b/synapse/push/emailpusher.py index c10deded06..3a13c7485a 100644 --- a/synapse/push/emailpusher.py +++ b/synapse/push/emailpusher.py @@ -30,6 +30,7 @@ DELAY_BEFORE_MAIL_MS = 2 * 60 * 1000 THROTTLE_START_MS = 2 * 60 * 1000 THROTTLE_MAX_MS = (2 * 60 * 1000) * (2 ** 11) # ~3 days +THROTTLE_MULTIPLIER = 2 # If no event triggers a notification for this long after the previous, # the throttle is released. @@ -231,7 +232,7 @@ class EmailPusher(object): new_throttle_ms = THROTTLE_START_MS else: new_throttle_ms = min( - current_throttle_ms * 2, + current_throttle_ms * THROTTLE_MULTIPLIER, THROTTLE_MAX_MS ) self.throttle_params[room_id] = { -- cgit 1.5.1 From 92f0f3d21d52ae14a3d4d4536a84055b92d228ae Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 May 2016 11:24:07 +0100 Subject: Catch all exceptions when creating a pusher --- synapse/push/pusherpool.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'synapse') diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 7fef2fb6f7..66eafb69d8 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -186,8 +186,8 @@ class PusherPool: for pusherdict in pushers: try: p = pusher.create_pusher(self.hs, pusherdict) - except PusherConfigException: - logger.exception("Couldn't start a pusher: caught PusherConfigException") + except: + logger.exception("Couldn't start a pusher: caught Exception") continue if p: appid_pushkey = "%s:%s" % ( -- cgit 1.5.1 From e6bffa4475e1a37643317d709b2003cdf99c149f Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 May 2016 11:26:58 +0100 Subject: Unused import --- synapse/push/pusherpool.py | 1 - 1 file changed, 1 deletion(-) (limited to 'synapse') diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py index 66eafb69d8..5853ec36a9 100644 --- a/synapse/push/pusherpool.py +++ b/synapse/push/pusherpool.py @@ -17,7 +17,6 @@ from twisted.internet import defer import pusher -from synapse.push import PusherConfigException from synapse.util.logcontext import preserve_fn from synapse.util.async import run_on_reactor -- 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') 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 f1026418ea0f2dba6cc4075b737ac6fa4ffac89d Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Wed, 4 May 2016 11:37:42 +0100 Subject: copyright --- synapse/push/pusher.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) (limited to 'synapse') diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index 25a45af775..d1eaa236bd 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -1,14 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright 2014-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 httppusher import HttpPusher +import logging +logger = logging.getLogger(__name__) def create_pusher(hs, pusherdict): + logger.info("trying to create_pusher for %r", pusherdict) + PUSHER_TYPES = { "http": HttpPusher, } + logger.info("email enable notifs: %r", hs.config.email_enable_notifs) if hs.config.email_enable_notifs: from emailpusher import EmailPusher PUSHER_TYPES["email"] = EmailPusher + logger.info("defined email pusher type") if pusherdict['kind'] in PUSHER_TYPES: + logger.info("found pusher") return PUSHER_TYPES[pusherdict['kind']](hs, pusherdict) -- cgit 1.5.1 From de22001ab55d7fb89e4fd1527a4b419f5c195458 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 May 2016 11:41:35 +0100 Subject: pep8 --- synapse/push/pusher.py | 1 + 1 file changed, 1 insertion(+) (limited to 'synapse') diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index d1eaa236bd..db5c1f1aa6 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -18,6 +18,7 @@ from httppusher import HttpPusher import logging logger = logging.getLogger(__name__) + def create_pusher(hs, pusherdict): logger.info("trying to create_pusher for %r", pusherdict) -- cgit 1.5.1 From 8cc82aad873d8c6c933a20d8c7552d061b4e6316 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 May 2016 11:47:59 +0100 Subject: Add db functions used for email to the pusher app --- synapse/app/pusher.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'synapse') diff --git a/synapse/app/pusher.py b/synapse/app/pusher.py index b5339f030d..9976908983 100644 --- a/synapse/app/pusher.py +++ b/synapse/app/pusher.py @@ -110,6 +110,18 @@ class PusherSlaveStore( DataStore.update_pusher_last_stream_ordering.__func__ ) + get_throttle_params_by_room = ( + DataStore.get_throttle_params_by_room.__func__ + ) + + set_throttle_params = ( + DataStore.set_throttle_params.__func__ + ) + + get_time_of_last_push_action_before = ( + DataStore.get_time_of_last_push_action_before.__func__ + ) + class PusherServer(HomeServer): -- cgit 1.5.1 From 80be39646467c46a52530cd0839746810ad32b62 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 May 2016 13:19:59 +0100 Subject: Correct SQL statement for postgres In standard sql, join binds tighter than comma, so we were joining on the wrong table. Postgres follows the standard (apparently). --- synapse/storage/event_push_actions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'synapse') diff --git a/synapse/storage/event_push_actions.py b/synapse/storage/event_push_actions.py index 85290d2a90..6f316f7d24 100644 --- a/synapse/storage/event_push_actions.py +++ b/synapse/storage/event_push_actions.py @@ -120,14 +120,15 @@ class EventPushActionsStore(SQLBaseStore): sql = ( "SELECT ep.event_id, ep.room_id, ep.stream_ordering, ep.actions, " "e.received_ts " - "FROM event_push_actions AS ep, (" + "FROM (" " SELECT room_id, user_id, " " max(topological_ordering) as topological_ordering, " " max(stream_ordering) as stream_ordering " " FROM events" " NATURAL JOIN receipts_linearized WHERE receipt_type = 'm.read'" " GROUP BY room_id, user_id" - ") AS rl" + ") AS rl," + " event_push_actions AS ep" " INNER JOIN events AS e USING (room_id, event_id)" " WHERE" " ep.room_id = rl.room_id" -- cgit 1.5.1 From 9ef05a12c380a6a5a45226d6086d70512fd4f073 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 May 2016 14:52:10 +0100 Subject: Add date header & message id --- synapse/app/pusher.py | 3 ++- synapse/push/mailer.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/app/pusher.py b/synapse/app/pusher.py index 9976908983..89c8d5c7ce 100644 --- a/synapse/app/pusher.py +++ b/synapse/app/pusher.py @@ -20,6 +20,7 @@ from synapse.server import HomeServer from synapse.config._base import ConfigError from synapse.config.database import DatabaseConfig from synapse.config.logger import LoggingConfig +from synapse.config.emailconfig import EmailConfig from synapse.http.site import SynapseSite from synapse.metrics.resource import MetricsResource, METRICS_PREFIX from synapse.replication.slave.storage.events import SlavedEventStore @@ -91,7 +92,7 @@ class SlaveConfig(DatabaseConfig): """ % locals() -class PusherSlaveConfig(SlaveConfig, LoggingConfig): +class PusherSlaveConfig(SlaveConfig, LoggingConfig, EmailConfig): pass diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 1e6654e2bb..037bc990d0 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -152,6 +152,8 @@ class Mailer(object): multipart_msg['Subject'] = "New Matrix Notifications" multipart_msg['From'] = self.hs.config.email_notif_from multipart_msg['To'] = email_address + multipart_msg['Date'] = email.utils.formatdate() + multipart_msg['Message-ID'] = email.utils.make_msgid() multipart_msg.attach(text_part) multipart_msg.attach(html_part) -- cgit 1.5.1 From 39d0a99972cc58e8d2c7f7997d13aa18d547a2d6 Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 4 May 2016 14:52:49 +0100 Subject: Include no context until we can de-dup between the context and other notifs --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 037bc990d0..ad44ba985e 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -44,7 +44,7 @@ MESSAGES_IN_ROOMS = "Here are some messages you may have missed" INVITE_FROM_PERSON_TO_ROOM = "%(person)s has invited you to join the %(room)s room" INVITE_FROM_PERSON = "%(person)s has invited you to chat" -CONTEXT_BEFORE = 1 +CONTEXT_BEFORE = 0 # From https://github.com/matrix-org/matrix-react-sdk/blob/master/src/HtmlUtils.js ALLOWED_TAGS = [ -- cgit 1.5.1 From 1cf5c379cb101c89897750d79e4548c172581f48 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 5 May 2016 01:54:12 +0100 Subject: spell out emailpusher full path --- synapse/push/pusher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/push/pusher.py b/synapse/push/pusher.py index db5c1f1aa6..e6c0806415 100644 --- a/synapse/push/pusher.py +++ b/synapse/push/pusher.py @@ -28,7 +28,7 @@ def create_pusher(hs, pusherdict): logger.info("email enable notifs: %r", hs.config.email_enable_notifs) if hs.config.email_enable_notifs: - from emailpusher import EmailPusher + from synapse.push.emailpusher import EmailPusher PUSHER_TYPES["email"] = EmailPusher logger.info("defined email pusher type") -- cgit 1.5.1 From ce81ccb063c46bf1bc3adca0d5dcfbc786624641 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 5 May 2016 01:56:43 +0100 Subject: handle fragments correctly on mxc URLs. switch to vector.im permalinks as matrix.to isn't ready yet. merge overlapping notifications together. give one message of context after a notification (in the unlikely event it exists, but it's possible thanks to throttling). include name of app in mail templates --- synapse/push/mailer.py | 59 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 14 deletions(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index ad44ba985e..5951c14ce1 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -35,16 +35,20 @@ import bleach import time import urllib +import logging +logger = logging.getLogger(__name__) -MESSAGE_FROM_PERSON_IN_ROOM = "You have a message from %(person)s in the %s room" -MESSAGE_FROM_PERSON = "You have a message from %(person)s" -MESSAGES_FROM_PERSON = "You have messages from %(person)s" -MESSAGES_IN_ROOM = "There are some messages for you in the %(room)s room" -MESSAGES_IN_ROOMS = "Here are some messages you may have missed" -INVITE_FROM_PERSON_TO_ROOM = "%(person)s has invited you to join the %(room)s room" -INVITE_FROM_PERSON = "%(person)s has invited you to chat" -CONTEXT_BEFORE = 0 +MESSAGE_FROM_PERSON_IN_ROOM = "You have a message on %%app%% 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..." +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%%..." + +CONTEXT_BEFORE = 1 +CONTEXT_AFTER = 1 # From https://github.com/matrix-org/matrix-react-sdk/blob/master/src/HtmlUtils.js ALLOWED_TAGS = [ @@ -181,7 +185,25 @@ class Mailer(object): notifvars = yield self.get_notif_vars( n, user_id, notif_events[n['event_id']], room_state ) - room_vars['notifs'].append(notifvars) + + # merge overlapping notifs together. + # relies on the notifs being in chronological order. + merge = False + if room_vars['notifs'] and 'messages' in room_vars['notifs'][-1]: + prev_messages = room_vars['notifs'][-1]['messages'] + for message in notifvars['messages']: + pm = filter(lambda pm: pm['id'] == message['id'], prev_messages) + if pm: + if not message["is_historical"]: + pm[0]["is_historical"] = False + merge = True + elif merge: + # we're merging, so append any remaining messages + # in this notif to the previous one + prev_messages.append(message) + + if not merge: + room_vars['notifs'].append(notifvars) defer.returnValue(room_vars) @@ -189,7 +211,7 @@ class Mailer(object): def get_notif_vars(self, notif, user_id, notif_event, room_state): results = yield self.store.get_events_around( notif['room_id'], notif['event_id'], - before_limit=CONTEXT_BEFORE, after_limit=0 + before_limit=CONTEXT_BEFORE, after_limit=CONTEXT_AFTER ) ret = { @@ -226,6 +248,7 @@ class Mailer(object): ret = { "msgtype": event.content["msgtype"], "is_historical": event.event_id != notif['event_id'], + "id": event.event_id, "ts": event.origin_server_ts, "sender_name": sender_name, "sender_avatar_url": sender_avatar_url, @@ -329,10 +352,12 @@ class Mailer(object): return MESSAGES_IN_ROOMS def make_room_link(self, room_id): - return "https://matrix.to/%s" % (room_id,) + # XXX: matrix.to + return "https://vector.im/#/room/%s" % (room_id,) def make_notif_link(self, notif): - return "https://matrix.to/%s/%s" % ( + # XXX: matrix.to + return "https://vector.im/#/room/%s/%s" % ( notif['room_id'], notif['event_id'] ) @@ -342,16 +367,22 @@ class Mailer(object): def mxc_to_http_filter(self, value, width, height, resize_method="crop"): if value[0:6] != "mxc://": return "" + serverAndMediaId = value[6:] + if '#' in serverAndMediaId: + (serverAndMediaId, fragment) = serverAndMediaId.split('#', 1) + fragment = "#" + fragment + params = { "width": width, "height": height, "method": resize_method, } - return "%s_matrix/media/v1/thumbnail/%s?%s" % ( + return "%s_matrix/media/v1/thumbnail/%s?%s%s" % ( self.hs.config.public_baseurl, serverAndMediaId, - urllib.urlencode(params) + urllib.urlencode(params), + fragment or "", ) -- cgit 1.5.1 From 634efb65f1ca8d3485c75a828a98161cfd617bb6 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 5 May 2016 02:10:57 +0100 Subject: pep8 --- synapse/push/mailer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 5951c14ce1..aab6387125 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -39,12 +39,14 @@ import logging logger = logging.getLogger(__name__) -MESSAGE_FROM_PERSON_IN_ROOM = "You have a message on %%app%% from %(person)s in the %s room..." +MESSAGE_FROM_PERSON_IN_ROOM = "You have a message on %%app%% 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..." -INVITE_FROM_PERSON_TO_ROOM = "%(person)s has invited you to join the %(room)s room on %%app%%..." +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%%..." CONTEXT_BEFORE = 1 -- 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') 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 53ca739f1f3a61f8605dac3f3ab9540dadda0178 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Thu, 5 May 2016 15:55:44 +0100 Subject: better mail subject lines --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 497905d811..f60ac94a83 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -156,7 +156,7 @@ class Mailer(object): text_part = MIMEText(plain_text, "plain", "utf8") multipart_msg = MIMEMultipart('alternative') - multipart_msg['Subject'] = "New Matrix Notifications" + multipart_msg['Subject'] = "[%s] %s" % (self.app_name, summary_text) multipart_msg['From'] = self.hs.config.email_notif_from multipart_msg['To'] = email_address multipart_msg['Date'] = email.utils.formatdate() -- cgit 1.5.1 From 55996088876664280e7a9232ca21b1875db6c413 Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Tue, 10 May 2016 00:14:48 +0200 Subject: Switch from CSS to Table layout for HTML mails so they work in Outlook aka Word Remove templates-vector and theme templates with variables instead Switch to matrix.to URLs by default for links --- res/templates-vector/mail.css | 115 ----------------------------- res/templates-vector/notif.html | 34 --------- res/templates-vector/notif.txt | 12 --- res/templates-vector/notif_mail.html | 25 ------- res/templates-vector/notif_mail.txt | 10 --- res/templates-vector/room.html | 27 ------- res/templates-vector/room.txt | 9 --- res/templates/mail.css | 137 +++++++++++++++++++++++++++++++++-- res/templates/notif.html | 50 +++++++------ res/templates/notif.txt | 2 +- res/templates/notif_mail.html | 42 ++++++++--- res/templates/room.html | 54 ++++++++------ synapse/push/mailer.py | 25 +++++-- 13 files changed, 240 insertions(+), 302 deletions(-) delete mode 100644 res/templates-vector/mail.css delete mode 100644 res/templates-vector/notif.html delete mode 100644 res/templates-vector/notif.txt delete mode 100644 res/templates-vector/notif_mail.html delete mode 100644 res/templates-vector/notif_mail.txt delete mode 100644 res/templates-vector/room.html delete mode 100644 res/templates-vector/room.txt (limited to 'synapse') diff --git a/res/templates-vector/mail.css b/res/templates-vector/mail.css deleted file mode 100644 index 103c5ff8ca..0000000000 --- a/res/templates-vector/mail.css +++ /dev/null @@ -1,115 +0,0 @@ -body { - margin: 0px; -} - -#page { - font-family: 'Open Sans', Helvetica, Arial, Sans-Serif; - font-size: 12pt; - - margin: auto; - max-width: 640px; - padding: 20px; -} - -.header { - height: 87px; - border-bottom: 4px solid #e4f7ed; -} - -.logo { - float: right; - margin-left: 20px; -} - -.salutation { - padding-top: 10px; - font-weight: bold; -} - -.summarytext { -} - -.room_header { - padding-top: 38px; - padding-bottom: 10px; - border-bottom: 1px solid #e5e5e5; -} - -.room_header h2 { - margin-top: 0px; - margin-left: 75px; - font-size: 20px; -} - -.room_avatar { - float: left; - margin-top: -8px; -} - -.room_avatar img { - width: 48px; - height: 48px; - object-fit: cover; - border-radius: 24px; - margin-left: 7px; -} - -.room_content { - clear: left; -} - -.notif { - border-bottom: 1px solid #e5e5e5; - margin-top: 16px; - padding-bottom: 16px; -} - -.historical { - opacity: 0.3; -} - -.message { - position: relative; - margin-bottom: 10px; -} - -.sender_avatar { - width: 32px; - height: 32px; - border-radius: 16px; - position: absolute; - margin-left: 14px; - margin-top: -2px; -} - -.sender_name { - margin-left: 75px; - display: inline; - font-size: 13px; - color: #a2a2a2; -} - -.message_time { - float: right; - font-size: 11px; - color: #a2a2a2; -} - -.message_body { - margin-left: 75px; -} - -.notif_link { - margin-left: 75px; - font-weight: bold; -} - -.notif_link a, .footer a { - color: #76CFA6; - text-decoration: none; -} - -.footer { - margin-top: 20px; - text-align: center; -} diff --git a/res/templates-vector/notif.html b/res/templates-vector/notif.html deleted file mode 100644 index 97ea425011..0000000000 --- a/res/templates-vector/notif.html +++ /dev/null @@ -1,34 +0,0 @@ -
-
- {% for message in notif.messages %} -
- {% if message.sender_avatar_url %} - - {% else %} - {% if message.sender_hash % 3 == 0 %} - - {% elif message.sender_hash % 3 == 1 %} - - {% else %} - - {% endif %} - - {% endif %} -
{{ message.sender_name }}
-
{{ message.ts|format_ts("%H:%M") }}
-
- {% if message.msgtype == "m.text" %} - {{ message.body_text_html }} - {% elif message.msgtype == "m.image" %} - - {% elif message.msgtype == "m.file" %} - {{ message.body_text_plain }} - {% endif %} -
-
- {% endfor %} -
- -
diff --git a/res/templates-vector/notif.txt b/res/templates-vector/notif.txt deleted file mode 100644 index b515f394c3..0000000000 --- a/res/templates-vector/notif.txt +++ /dev/null @@ -1,12 +0,0 @@ -{% 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-vector/notif_mail.html b/res/templates-vector/notif_mail.html deleted file mode 100644 index c49cc66548..0000000000 --- a/res/templates-vector/notif_mail.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - -
-
- -
Hi {{ user_display_name }},
-
{{ summary_text }}
-
-
- {% for room in rooms %} - {% include 'room.html' with context %} - {% endfor %} -
- -
- - diff --git a/res/templates-vector/notif_mail.txt b/res/templates-vector/notif_mail.txt deleted file mode 100644 index 24843042a5..0000000000 --- a/res/templates-vector/notif_mail.txt +++ /dev/null @@ -1,10 +0,0 @@ -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-vector/room.html b/res/templates-vector/room.html deleted file mode 100644 index ab11492578..0000000000 --- a/res/templates-vector/room.html +++ /dev/null @@ -1,27 +0,0 @@ -
-
-
- {% if room.avatar_url %} - - {% else %} - {% if room.hash % 3 == 0 %} - - {% elif room.hash % 3 == 1 %} - - {% else %} - - {% endif %} - {% endif %} -
-

{{ room.title }}

-
-
- {% if room.invite %} - Join the conversation. - {% else %} - {% for notif in room.notifs %} - {% include 'notif.html' with context %} - {% endfor %} - {% endif %} -
-
diff --git a/res/templates-vector/room.txt b/res/templates-vector/room.txt deleted file mode 100644 index 84648c710e..0000000000 --- a/res/templates-vector/room.txt +++ /dev/null @@ -1,9 +0,0 @@ -{{ room.title }} - -{% if room.invite %} - You've been invited, join at {{ room.link }} -{% else %} - {% for notif in room.notifs %} - {% include 'notif.txt' with context %} - {% endfor %} -{% endif %} diff --git a/res/templates/mail.css b/res/templates/mail.css index 61953ba51c..b02f509e58 100644 --- a/res/templates/mail.css +++ b/res/templates/mail.css @@ -1,21 +1,146 @@ +body { + margin: 0px; +} + +#page { + font-family: 'Open Sans', Helvetica, Arial, Sans-Serif; + font-color: #454545; + font-size: 12pt; + width: 100%; + padding: 20px; +} + +#inner { + width: 640px; +} + +.header { + width: 100%; + height: 87px; + color: #454545; + border-bottom: 4px solid #e5e5e5; +} + +.logo { + text-align: right; + margin-left: 20px; +} + +.salutation { + padding-top: 10px; + font-weight: bold; +} + +.summarytext { +} + +.room { + width: 100%; + color: #454545; + border-bottom: 1px solid #e5e5e5; +} + +.room_header td { + padding-top: 38px; + padding-bottom: 10px; + border-bottom: 1px solid #e5e5e5; +} + +.room_name { + vertical-align: middle; + font-size: 18px; + font-weight: bold; +} + +.room_header h2 { + margin-top: 0px; + margin-left: 75px; + font-size: 20px; +} + .room_avatar { + width: 56px; + line-height: 0px; + text-align: center; + vertical-align: middle; +} + +.room_avatar img { width: 48px; height: 48px; - float: left; + object-fit: cover; + border-radius: 24px; +} + +.notif { + border-bottom: 1px solid #e5e5e5; + margin-top: 16px; + padding-bottom: 16px; +} + +.historical_message .sender_avatar { + opacity: 0.3; +} + +/* spell out opacity and historical_message class names for Outlook aka Word */ +.historical_message .sender_name { + color: #e3e3e3; +} + +.historical_message .message_time { + color: #e3e3e3; +} + +.historical_message .message_body { + color: #c7c7c7; +} + +.historical_message td, +.message td { + padding-top: 10px; } -.room_content { - clear: left; +.sender_avatar { + width: 56px; + text-align: center; + vertical-align: top; } -.historical { - color: #888; +.sender_avatar img { + margin-top: -2px; + width: 32px; + height: 32px; + border-radius: 16px; } .sender_name { display: inline; + font-size: 13px; + color: #a2a2a2; } .message_time { - display: inline; + text-align: right; + width: 100px; + font-size: 11px; + color: #a2a2a2; +} + +.message_body { } + +.notif_link td { + padding-top: 10px; + padding-bottom: 10px; + font-weight: bold; +} + +.notif_link a, .footer a { + color: #454545; + text-decoration: none; +} + +.footer { + margin-top: 20px; + text-align: center; +} \ No newline at end of file diff --git a/res/templates/notif.html b/res/templates/notif.html index 3112df9704..92e80352fc 100644 --- a/res/templates/notif.html +++ b/res/templates/notif.html @@ -1,9 +1,9 @@ -
-
- {% for message in notif.messages %} -
+{% for message in notif.messages %} + + + {% if loop.index0 == 0 or notif.messages[loop.index0 - 1].sender_name != notif.messages[loop.index0].sender_name %} {% if message.sender_avatar_url %} - + {% else %} {% if message.sender_hash % 3 == 0 %} @@ -14,21 +14,29 @@ {% endif %} {% endif %} + {% endif %} + + + {% if loop.index0 == 0 or notif.messages[loop.index0 - 1].sender_name != notif.messages[loop.index0].sender_name %}
{{ message.sender_name }}
-
{{ message.ts|format_ts("%H:%M") }}
-
- {% if message.msgtype == "m.text" %} - {{ message.body_text_html }} - {% elif message.msgtype == "m.image" %} - - {% elif message.msgtype == "m.file" %} - {{ message.body_text_plain }} - {% endif %} -
+ {% endif %} +
+ {% if message.msgtype == "m.text" %} + {{ message.body_text_html }} + {% elif message.msgtype == "m.image" %} + + {% elif message.msgtype == "m.file" %} + {{ message.body_text_plain }} + {% endif %}
- {% endfor %} -
- -
+ + {{ message.ts|format_ts("%H:%M") }} + +{% endfor %} + + + + View {{ room.title }} + + + diff --git a/res/templates/notif.txt b/res/templates/notif.txt index b515f394c3..a3ddac80ce 100644 --- a/res/templates/notif.txt +++ b/res/templates/notif.txt @@ -9,4 +9,4 @@ {% endif %} {% endfor %} -View at {{ notif.link }} +View {{ room.title }} at {{ notif.link }} diff --git a/res/templates/notif_mail.html b/res/templates/notif_mail.html index cc3573e65a..4034f3c601 100644 --- a/res/templates/notif_mail.html +++ b/res/templates/notif_mail.html @@ -3,20 +3,38 @@ -
-
Hi {{ user_display_name }},
-
{{ summary_text }}
-
- {% for room in rooms %} - {% include 'room.html' with context %} - {% endfor %} -
- -
+ + + + + + +
+ + + + + +
+
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 3c0a4607b3..723c222d25 100644 --- a/res/templates/room.html +++ b/res/templates/room.html @@ -1,25 +1,33 @@ -
-
- {% if room.avatar_url %} - - {% else %} - {% if room.hash % 3 == 0 %} - - {% elif room.hash % 3 == 1 %} - + + + + + + {% if room.invite %} + + + + + + {% else %} + {% for notif in room.notifs %} + {% include 'notif.html' with context %} + {% endfor %} + {% endif %} +
+ {% if room.avatar_url %} + {% else %} - + {% if room.hash % 3 == 0 %} + + {% elif room.hash % 3 == 1 %} + + {% else %} + + {% endif %} {% endif %} - {% endif %} - -

{{ room.title }}

-
- {% if room.invite %} - Join the conversation. - {% else %} - {% for notif in room.notifs %} - {% include 'notif.html' with context %} - {% endfor %} - {% endif %} -
- +
+ {{ room.title }} +
+ Join the conversation. +
diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index f60ac94a83..3c38321fdd 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -146,6 +146,7 @@ class Mailer(object): "user_display_name": user_display_name, "unsubscribe_link": self.make_unsubscribe_link(), "summary_text": summary_text, + "app_name": self.app_name, "rooms": rooms, } @@ -164,6 +165,9 @@ class Mailer(object): multipart_msg.attach(text_part) multipart_msg.attach(html_part) + logger.info("Sending email push notification to %s" % email_address) + #logger.debug(html_text) + yield sendmail( self.hs.config.email_smtp_host, raw_from, raw_to, multipart_msg.as_string(), @@ -367,19 +371,26 @@ class Mailer(object): } def make_room_link(self, room_id): - # XXX: matrix.to # need /beta for Universal Links to work on iOS - return "https://vector.im/beta/#/room/%s" % (room_id,) + if self.app_name == "Vector": + return "https://vector.im/beta/#/room/%s" % (room_id,) + else: + return "https://matrix.to/#/room/%s" % (room_id,) def make_notif_link(self, notif): - # XXX: matrix.to # need /beta for Universal Links to work on iOS - return "https://vector.im/beta/#/room/%s/%s" % ( - notif['room_id'], notif['event_id'] - ) + if self.app_name == "Vector": + return "https://vector.im/beta/#/room/%s/%s" % ( + notif['room_id'], notif['event_id'] + ) + else: + return "https://matrix.to/#/room/%s/%s" % ( + notif['room_id'], notif['event_id'] + ) def make_unsubscribe_link(self): - return "https://vector.im/#/settings" # XXX: matrix.to + # XXX: matrix.to + return "https://vector.im/#/settings" def mxc_to_http_filter(self, value, width, height, resize_method="crop"): if value[0:6] != "mxc://": -- cgit 1.5.1 From e04b1d6b0a6b6f2934e59e73a605e99da6ca9f5e Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 10 May 2016 14:23:16 +0200 Subject: Make pep8 happy --- synapse/push/mailer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'synapse') diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py index 3c38321fdd..7031fa6d55 100644 --- a/synapse/push/mailer.py +++ b/synapse/push/mailer.py @@ -166,7 +166,7 @@ class Mailer(object): multipart_msg.attach(html_part) logger.info("Sending email push notification to %s" % email_address) - #logger.debug(html_text) + # logger.debug(html_text) yield sendmail( self.hs.config.email_smtp_host, -- 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') 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') 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