]> git.ipfire.org Git - pbs.git/commitdiff
messages: Refactor module
authorMichael Tremer <michael.tremer@ipfire.org>
Wed, 1 Nov 2017 17:28:22 +0000 (17:28 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Wed, 1 Nov 2017 17:30:49 +0000 (17:30 +0000)
Adds the possibility to use HTML/TXT/markdown templates

Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
Dockerfile.in
Makefile.am
src/buildservice/__init__.py
src/buildservice/messages.py
src/buildservice/users.py
src/database.sql
src/templates/messages/users/account-activation.markdown [new file with mode: 0644]

index 13c6097f75c6eaf5fb98a7fa26daaf053e0cd7ee..b9200d85626cdfad62295c2742678181cc1bb9d9 100644 (file)
@@ -15,6 +15,7 @@ RUN yum install -y \
        python2-pip \
        python-daemon \
        python-ldap \
+       python-markdown \
        python-memcached \
        python-psycopg2 \
        python-tornado \
index bc5cd946890770914f3f44544ec1838a9c23d9b2..d5aed69798b1a8e7c9550e2cda02d5d07c2ce3dc 100644 (file)
@@ -241,6 +241,13 @@ dist_templates_errors_DATA = \
 
 templates_errorsdir = $(templatesdir)/errors
 
+templates_messagesdir = $(templatesdir)/messages
+
+dist_templates_messages_users_DATA = \
+       src/templates/messages/users/account-activation.markdown
+
+templates_messages_usersdir = $(templates_messagesdir)/users
+
 dist_templates_mirrors_DATA = \
        src/templates/mirrors/delete.html \
        src/templates/mirrors/detail.html \
index a340aa329790d5bfadd5b062ac1473fd33419395..c3d5ed08698007467fec56ed3dcbc25d2161a128 100644 (file)
@@ -40,6 +40,8 @@ from .decorators import *
 from .constants import *
 
 class Backend(object):
+       version = __version__
+
        def __init__(self, config_file=None):
                # Read configuration file.
                self.config = self.read_config(config_file)
index cd2bd923b8ab22942c8c4e5f627494dc9d4b8dad..14f63436d543b7859ff4a2f570ef8408f63b95cb 100644 (file)
 #!/usr/bin/python
 
+import email
+import email.mime.multipart
+import email.mime.text
 import logging
-import smtplib
+import markdown
 import subprocess
 import tornado.locale
-
-from email.mime.text import MIMEText
+import tornado.template
 
 from . import base
+from . import users
+
+from .constants import TEMPLATESDIR
 
 class Messages(base.Object):
-       def add(self, to, subject, text, frm=None):
-               subject = "%s %s" % (self.pakfire.settings.get("email_subject_prefix"), subject)
+       def init(self):
+               self.templates = tornado.template.Loader(TEMPLATESDIR)
 
-               # Get default sender from the settings.
-               if not frm:
-                       frm = self.pakfire.settings.get("email_from")
+       def __iter__(self):
+               messages = self.db.query("SELECT * FROM messages \
+                       WHERE sent_at IS NULL ORDER BY queued_at")
 
-               self.db.execute("INSERT INTO user_messages(frm, \"to\", subject, text)"
-                       " VALUES(%s, %s, %s, %s)", frm, to, subject, text)
+               return iter(messages)
 
-       def get_all(self, limit=None):
-               query = "SELECT * FROM user_messages ORDER BY time_added ASC"
-               if limit:
-                       query += " LIMIT %d" % limit
+       def __len__(self):
+               res = self.db.get("SELECT COUNT(*) AS count FROM messages \
+                       WHERE sent_at IS NULL")
 
-               return self.db.query(query)
+               return res.count
 
-       @property
-       def count(self):
-               ret = self.db.get("SELECT COUNT(*) as count FROM user_messages")
+       def process_queue(self):
+               """
+                       Sends all emails in the queue
+               """
+               for message in self:
+                       with self.db.transaction():
+                               self.__sendmail(message)
+
+               # Delete all old emails
+               with self.db.transaction():
+                       self.cleanup()
+
+       def cleanup(self):
+               self.db.execute("DELETE FROM messages WHERE sent_at <= NOW() - INTERVAL '24 hours'")
+
+       def send_to(self, recipient, message, sender=None, headers={}):
+               # Parse the message
+               if not isinstance(message, email.message.Message):
+                       message = email.message_from_string(message)
+
+               if not sender:
+                       sender = self.backend.settings.get("email_from", "Pakfire Build Service <no-reply@ipfire.org>")
+
+               # Add sender
+               message.add_header("From", sender)
+
+               # Add recipient
+               message.add_header("To", recipient)
+
+               # Sending this message now
+               message.add_header("Date", email.utils.formatdate())
+
+               # Add sender program
+               message.add_header("X-Mailer", "Pakfire Build Service %s" % self.backend.version)
+
+               # Add any headers
+               for k, v in headers.items():
+                       message.add_header(k, v)
+
+               # Queue the message
+               self.queue(message.as_string())
+
+       def send_template(self, recipient, name, sender=None, headers={}, **kwargs):
+               # Get user (if we have one)
+               if isinstance(recipient, users.User):
+                       user = recipient
+               else:
+                       user = self.backend.users.find_maintainer(recipient)
+
+               # Get the user's locale or use default
+               if user:
+                       locale = user.locale
+               else:
+                       locale = tornado.locale.get()
+
+               # Create namespace
+               namespace = {
+                       "baseurl"       : self.settings.get("baseurl"),
+                       "recipient"     : recipient,
+                       "user"          : user,
+
+                       # Locale
+                       "locale"        : locale,
+                       "_"                     : locale.translate,
+               }
+               namespace.update(kwargs)
+
+               # Create a MIMEMultipart message.
+               message = email.mime.multipart.MIMEMultipart()
+
+               # Create an alternating multipart message to show HTML or text
+               alternative = email.mime.multipart.MIMEMultipart("alternative")
+
+               for fmt, mimetype in (("txt", "plain"), ("html", "html"), ("markdown", "html")):
+                       try:
+                               t = self.templates.load("%s.%s" % (name, fmt))
+                       except IOError:
+                               continue
 
-               return ret.count
+                       # Render the message
+                       try:
+                               part = t.generate(**namespace)
 
-       def delete(self, id):
-               self.db.execute("DELETE FROM user_messages WHERE id = %s", id)
+                       # Reset the rendered template when it could not be rendered
+                       except:
+                               self.templates.reset()
+                               raise
 
-       def process_queue(self):
-               # Get 10 messages at a time and send them one after the other
-               while True:
-                       messages = self.get_all(limit=10)
+                       # Parse the message
+                       part = email.message_from_string(part)
 
-                       # If no emails are available, we end here
-                       if not messages:
-                               break
+                       # Extract the headers
+                       for k, v in part.items():
+                               message.add_header(k, v)
 
-                       for message in messages:
-                               with self.db.transaction():
-                                       self.send_msg(message)
+                       body = part.get_payload()
 
-       def send_to_all(self, recipients, subject, body, format=None):
-               """
-                       Sends an email to all recipients and does the translation.
-               """
-               if not format:
-                       format = {}
+                       # Render markdown
+                       if fmt == "markdown":
+                               body = markdown.markdown(body)
 
-               for recipient in recipients:
-                       if not recipient:
-                               logging.warning("Ignoring empty recipient.")
-                               continue
+                       # Compile part again
+                       part = email.mime.text.MIMEText(body, mimetype, "utf-8")
+
+                       # Attach the parts to the mime container
+                       # According to RFC2046, the last part of a multipart message is preferred
+                       alternative.attach(part)
+
+               # Add alternative section to outer message
+               message.attach(alternative)
+
+               # Send the message
+               self.send_to(user.email.recipient if user else recipient, message, sender=sender, headers=headers)
+
+       def queue(self, message):
+               res = self.db.get("INSERT INTO messages(message) VALUES(%s) RETURNING id", message)
+
+               logging.info("Message queued as %s", res.id)
+
+       def __sendmail(self, message):
+               # Convert message from string
+               msg = email.message_from_string(message.message)
+
+               # Get some headers
+               recipient = msg.get("To")
+               subject   = msg.get("Subject")
+
+               logging.info("Sending mail to %s: %s" % (recipient, subject))
 
-                       # We try to get more information about the user from the database
-                       # like the locale.
-                       user = self.pakfire.users.get_by_email(recipient)
-                       if user:
-                               # Get locale that the user prefers.
-                               locale = tornado.locale.get(user.locale)
-                       else:
-                               # Get the default locale.
-                               locale = tornado.locale.get()
-
-                       # Translate the message.
-                       _subject = locale.translate(subject) % format
-                       _body    = locale.translate(body) % format
-
-                       # If we know the real name of the user we add the realname to
-                       # the recipient field.
-                       if user:
-                               recipient = "%s <%s>" % (user.realname, user.email)
-
-                       # Add the message to the queue that it is sent.
-                       self.add(recipient, _subject, _body)
-
-       def send_msg(self, msg):
-               if not msg.to:
-                       logging.warning("Dropping message with empty recipient.")
-                       return
-
-               logging.debug("Sending mail to %s: %s" % (msg.to, msg.subject))
-
-               # Preparing mail content.
-               mail = MIMEText(msg.text.encode("latin-1"))
-               mail["From"] = msg.frm.encode("latin-1")
-               mail["To"] = msg.to.encode("latin-1")
-               mail["Subject"] = msg.subject.encode("latin-1")
-               #mail["Content-type"] = "text/plain; charset=utf-8"
-
-               #smtp = smtplib.SMTP("localhost")
-               #smtp.sendmail(msg.frm, msg.to.split(", "), mail.as_string())
-               #smtp.quit()
-
-               # We use sendmail here to workaround problems with the mailserver
-               # communication.
-               # So, just call /usr/lib/sendmail, pipe the message in and see
-               # what sendmail tells us in return.
-               sendmail = ["/usr/lib/sendmail", "-t"]
-               p = subprocess.Popen(sendmail, bufsize=0, close_fds=True,
+               # Run sendmail and the email in
+               p = subprocess.Popen(["/usr/lib/sendmail", "-t"], bufsize=0, close_fds=True,
                        stdin=subprocess.PIPE, stdout=subprocess.PIPE)
 
-               stdout, stderr = p.communicate(mail.as_string())
+               stdout, stderr = p.communicate(msg.as_string())
 
                # Wait until sendmail has finished.
                p.wait()
@@ -117,5 +171,5 @@ class Messages(base.Object):
                if p.returncode:
                        raise Exception, "Could not send mail: %s" % stderr
 
-               # If everything was okay, we can delete the message in the database.
-               self.delete(msg.id)
+               # Mark message as sent
+               self.db.execute("UPDATE messages SET sent_at = NOW() WHERE id = %s", message.id)
index 728d6d4d5c2cd72311e30b84e9ef80ee070a56c9..3682ab8eae1ce91290d99058882c539daa11962f 100644 (file)
@@ -401,6 +401,9 @@ class User(base.DataObject):
 
                return user_email
 
+       def send_template(self, *args, **kwargs):
+               return self.backend.messages.send_template(self, *args, **kwargs)
+
        def set_state(self, state):
                self._set_attribute("state", state)
 
@@ -536,22 +539,7 @@ class UserEmail(base.DataObject):
 
                logging.debug("Sending activation mail to %s" % self.email)
 
-               # Get the saved locale from the user.
-               _ = tornado.locale.get(self.user.locale).translate
-
-               subject = _("Account Activation")
-
-               message  = _("You, or somebody using your email address, has registered an account on the Pakfire Build Service.")
-               message += "\n"*2
-               message += _("To activate your account, please click on the link below.")
-               message += "\n"*2
-               message += "    %(baseurl)s/user/%(name)s/activate?code=%(activation_code)s" \
-                       % { "baseurl" : self.settings.get("baseurl"), "name" : self.user.name,
-                               "activation_code" : self.activation_code, }
-               message += "\n"*2
-               message += "Sincerely,\n    The Pakfire Build Service"
-
-               self.backend.messages.add(self.recipient, subject, message)
+               self.user.send_template("messages/users/account-activation")
 
        def send_email_activation_mail(self, email):
                logging.debug("Sending email address activation mail to %s" % self.email)
index 40775333d261b748812d508e07f32820e017f50c..dd6d0b9d6600be6269652d0e8dc69c8c28dc7a95 100644 (file)
@@ -1006,6 +1006,41 @@ ALTER TABLE logfiles_id_seq OWNER TO pakfire;
 ALTER SEQUENCE logfiles_id_seq OWNED BY logfiles.id;
 
 
+--
+-- Name: messages; Type: TABLE; Schema: public; Owner: pakfire; Tablespace: 
+--
+
+CREATE TABLE messages (
+    id integer NOT NULL,
+    message text NOT NULL,
+    queued_at timestamp without time zone DEFAULT now() NOT NULL,
+    sent_at timestamp without time zone
+);
+
+
+ALTER TABLE messages OWNER TO pakfire;
+
+--
+-- Name: messages_id_seq; Type: SEQUENCE; Schema: public; Owner: pakfire
+--
+
+CREATE SEQUENCE messages_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+
+ALTER TABLE messages_id_seq OWNER TO pakfire;
+
+--
+-- Name: messages_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: pakfire
+--
+
+ALTER SEQUENCE messages_id_seq OWNED BY messages.id;
+
+
 --
 -- Name: mirrors; Type: TABLE; Schema: public; Owner: pakfire; Tablespace: 
 --
@@ -1580,43 +1615,6 @@ ALTER TABLE uploads_id_seq OWNER TO pakfire;
 ALTER SEQUENCE uploads_id_seq OWNED BY uploads.id;
 
 
---
--- Name: user_messages; Type: TABLE; Schema: public; Owner: pakfire; Tablespace: 
---
-
-CREATE TABLE user_messages (
-    id integer NOT NULL,
-    frm text NOT NULL,
-    "to" text NOT NULL,
-    subject text NOT NULL,
-    text text NOT NULL,
-    time_added timestamp without time zone DEFAULT now() NOT NULL
-);
-
-
-ALTER TABLE user_messages OWNER TO pakfire;
-
---
--- Name: user_messages_id_seq; Type: SEQUENCE; Schema: public; Owner: pakfire
---
-
-CREATE SEQUENCE user_messages_id_seq
-    START WITH 1
-    INCREMENT BY 1
-    NO MINVALUE
-    NO MAXVALUE
-    CACHE 1;
-
-
-ALTER TABLE user_messages_id_seq OWNER TO pakfire;
-
---
--- Name: user_messages_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: pakfire
---
-
-ALTER SEQUENCE user_messages_id_seq OWNED BY user_messages.id;
-
-
 --
 -- Name: users; Type: TABLE; Schema: public; Owner: pakfire; Tablespace: 
 --
@@ -1853,6 +1851,13 @@ ALTER TABLE ONLY keys_subkeys ALTER COLUMN id SET DEFAULT nextval('keys_subkeys_
 ALTER TABLE ONLY logfiles ALTER COLUMN id SET DEFAULT nextval('logfiles_id_seq'::regclass);
 
 
+--
+-- Name: id; Type: DEFAULT; Schema: public; Owner: pakfire
+--
+
+ALTER TABLE ONLY messages ALTER COLUMN id SET DEFAULT nextval('messages_id_seq'::regclass);
+
+
 --
 -- Name: id; Type: DEFAULT; Schema: public; Owner: pakfire
 --
@@ -1944,13 +1949,6 @@ ALTER TABLE ONLY sources_commits ALTER COLUMN id SET DEFAULT nextval('sources_co
 ALTER TABLE ONLY uploads ALTER COLUMN id SET DEFAULT nextval('uploads_id_seq'::regclass);
 
 
---
--- Name: id; Type: DEFAULT; Schema: public; Owner: pakfire
---
-
-ALTER TABLE ONLY user_messages ALTER COLUMN id SET DEFAULT nextval('user_messages_id_seq'::regclass);
-
-
 --
 -- Name: id; Type: DEFAULT; Schema: public; Owner: pakfire
 --
@@ -2240,7 +2238,7 @@ ALTER TABLE ONLY users_permissions
 -- Name: idx_2198274_primary; Type: CONSTRAINT; Schema: public; Owner: pakfire; Tablespace: 
 --
 
-ALTER TABLE ONLY user_messages
+ALTER TABLE ONLY messages
     ADD CONSTRAINT idx_2198274_primary PRIMARY KEY (id);
 
 
@@ -2582,6 +2580,13 @@ CREATE INDEX jobs_time_finished ON jobs USING btree (time_finished DESC) WHERE (
 CREATE INDEX jobs_time_started ON jobs USING btree (time_started) WHERE ((time_started IS NOT NULL) AND (time_finished IS NULL));
 
 
+--
+-- Name: messages_order; Type: INDEX; Schema: public; Owner: pakfire; Tablespace: 
+--
+
+CREATE INDEX messages_order ON messages USING btree (queued_at) WHERE (sent_at IS NULL);
+
+
 --
 -- Name: mirrors_checks_sort; Type: INDEX; Schema: public; Owner: pakfire; Tablespace: 
 --
diff --git a/src/templates/messages/users/account-activation.markdown b/src/templates/messages/users/account-activation.markdown
new file mode 100644 (file)
index 0000000..379c1a3
--- /dev/null
@@ -0,0 +1,10 @@
+Subject: {{ _("Account Activation") }}
+
+{{ _("You, or somebody using your email address, has registered an account on the Pakfire Build Service.") }}
+
+{{ _("To activate your account, please click on the link below:") }}
+
+  {{ baseurl }}/user/{{ user.name }}/activate?code={{ user.email.activation_code }}
+
+Sincerely,  
+-The Pakfire Build Service
\ No newline at end of file