From 9101b953e4434d1a50aa42d7329dfb8223a0b333 Mon Sep 17 00:00:00 2001
From: Dan Goodliffe <dan@randomdan.homeip.net>
Date: Fri, 2 Dec 2016 21:27:50 +0000
Subject: Basic notification service implementation

---
 gentoobrowse-api/service/Jamfile.jam               |   7 +-
 gentoobrowse-api/service/main.cpp                  |   4 +
 gentoobrowse-api/service/notifications/Jamfile.jam |  44 +++++++++
 .../service/notifications/mailserverimpl.cpp       |  64 +++++++++++++
 .../service/notifications/mailserverimpl.h         |  20 ++++
 .../service/notifications/notifications.ice        |  49 ++++++++++
 .../service/notifications/notificationsimpl.cpp    |  59 ++++++++++++
 .../service/notifications/notificationsimpl.h      |  29 ++++++
 .../service/notifications/xslt/base.xslt           |  61 ++++++++++++
 .../service/notifications/xslt/news.xslt           |  57 ++++++++++++
 .../service/notifications/xslt/signup.xslt         |  24 +++++
 .../service/notifications/xsltStreamSerializer.cpp |  82 +++++++++++++++++
 .../service/notifications/xsltStreamSerializer.h   |  28 ++++++
 gentoobrowse-api/unittests/Jamfile.jam             |   8 ++
 gentoobrowse-api/unittests/mockDefs.cpp            |   5 +
 gentoobrowse-api/unittests/mockDefs.h              |   3 +
 gentoobrowse-api/unittests/testNotifications.cpp   | 102 +++++++++++++++++++++
 17 files changed, 645 insertions(+), 1 deletion(-)
 create mode 100644 gentoobrowse-api/service/notifications/Jamfile.jam
 create mode 100644 gentoobrowse-api/service/notifications/mailserverimpl.cpp
 create mode 100644 gentoobrowse-api/service/notifications/mailserverimpl.h
 create mode 100644 gentoobrowse-api/service/notifications/notifications.ice
 create mode 100644 gentoobrowse-api/service/notifications/notificationsimpl.cpp
 create mode 100644 gentoobrowse-api/service/notifications/notificationsimpl.h
 create mode 100644 gentoobrowse-api/service/notifications/xslt/base.xslt
 create mode 100644 gentoobrowse-api/service/notifications/xslt/news.xslt
 create mode 100644 gentoobrowse-api/service/notifications/xslt/signup.xslt
 create mode 100644 gentoobrowse-api/service/notifications/xsltStreamSerializer.cpp
 create mode 100644 gentoobrowse-api/service/notifications/xsltStreamSerializer.h
 create mode 100644 gentoobrowse-api/unittests/testNotifications.cpp

diff --git a/gentoobrowse-api/service/Jamfile.jam b/gentoobrowse-api/service/Jamfile.jam
index a065393..7300b68 100644
--- a/gentoobrowse-api/service/Jamfile.jam
+++ b/gentoobrowse-api/service/Jamfile.jam
@@ -4,10 +4,11 @@ lib icetray : : : : <include>/usr/include/icetray ;
 lib git2 ;
 
 lib gentoobrowse-service :
-	[ glob-tree *.cpp : bin ]
+	[ glob-tree *.cpp : bin notifications ]
 	[ glob-tree *.sql ]
 	:
 	<library>..//adhocutil
+	<library>notifications
 	<library>icetray
 	<library>git2
 	<library>..//dbppcore
@@ -15,6 +16,8 @@ lib gentoobrowse-service :
 	<library>..//slicer
 	<library>..//slicer-db
 	<library>../api//gentoobrowse-api
+	<library>notifications//gentoobrowse-service-notifications
+	<implicit-dependency>notifications//gentoobrowse-service-notifications
 	<implicit-dependency>../api//gentoobrowse-api
 	<library>..//boost_system
 	<library>..//boost_thread
@@ -27,6 +30,8 @@ lib gentoobrowse-service :
 	: :
 	<include>.
 	<implicit-dependency>../api//gentoobrowse-api
+	<library>notifications//gentoobrowse-service-notifications
+	<implicit-dependency>notifications//gentoobrowse-service-notifications
 	<library>../api//gentoobrowse-api
 	;
 
diff --git a/gentoobrowse-api/service/main.cpp b/gentoobrowse-api/service/main.cpp
index bdf6ed5..b7b18a2 100644
--- a/gentoobrowse-api/service/main.cpp
+++ b/gentoobrowse-api/service/main.cpp
@@ -4,6 +4,8 @@
 #include "portageimpl.h"
 #include "usersimpl.h"
 #include "maintenanceimpl.h"
+#include "notificationsimpl.h"
+#include "mailserverimpl.h"
 
 namespace Gentoo {
 namespace Service {
@@ -15,6 +17,8 @@ class Api : public IceTray::Service {
 			adp->add(new Portage(db), ic->stringToIdentity("portage"));
 			adp->add(new Maintenance(db), ic->stringToIdentity("maintenance"));
 			adp->add(new Users(db), ic->stringToIdentity("users"));
+			adp->add(new Notifications(), ic->stringToIdentity("notifications"));
+			adp->add(new MailServer(), ic->stringToIdentity("mailserver"));
 		}
 };
 
diff --git a/gentoobrowse-api/service/notifications/Jamfile.jam b/gentoobrowse-api/service/notifications/Jamfile.jam
new file mode 100644
index 0000000..a168649
--- /dev/null
+++ b/gentoobrowse-api/service/notifications/Jamfile.jam
@@ -0,0 +1,44 @@
+import type : register ;
+import generators : register-standard ;
+
+type.register XSLT : xslt ;
+
+generators.register-standard xslt.h : XSLT : H ;
+
+h base-xslt : xslt/base.xslt : <slicer>yes ;
+h signup-xslt : xslt/signup.xslt : <slicer>yes ;
+h news-xslt : xslt/news.xslt : <slicer>yes ;
+
+lib exslt ;
+lib xslt : : : : <include>/usr/include/libxslt ;
+lib xml2 : : : : <include>/usr/include/libxml2 ;
+lib esmtp ;
+lib slicer-xml ;
+lib boost_system ;
+
+lib gentoobrowse-service-notifications :
+	[ glob *.ice *.cpp ]
+	:
+	<slicer>yes
+	<library>xslt
+	<library>xml2
+	<library>esmtp
+	<library>../../..//libxmlpp
+	<library>boost_system
+	<library>slicer-xml
+	<library>../../domain//gentoobrowse-domain
+	<implicit-dependency>../../domain//gentoobrowse-domain
+	<dependency>base-xslt
+	<dependency>signup-xslt
+	<dependency>news-xslt
+	: :
+	<include>.
+	;
+
+actions xslt.h
+{
+	( cd $(2:D) ; xxd -i $(2:B)$(2:S) ) > $(1)
+}
+
+IMPORT $(__name__) : xslt.h : : xslt.h ;
+
diff --git a/gentoobrowse-api/service/notifications/mailserverimpl.cpp b/gentoobrowse-api/service/notifications/mailserverimpl.cpp
new file mode 100644
index 0000000..c3dfdfa
--- /dev/null
+++ b/gentoobrowse-api/service/notifications/mailserverimpl.cpp
@@ -0,0 +1,64 @@
+#include "mailserverimpl.h"
+#include <libesmtp.h>
+#include <Ice/ObjectAdapter.h>
+#include <Ice/Communicator.h>
+
+namespace Gentoo {
+	namespace Service {
+		typedef std::vector<std::string> Parts;
+		typedef std::pair<Parts::iterator, Parts::const_iterator> PartsProgress;
+
+		void
+		MailServer::sendEmail(const Gentoo::EmailPtr & msg, const Ice::Current & c)
+		{
+			auto props = c.adapter->getCommunicator()->getProperties();
+			smtp_session_t session = smtp_create_session();
+			smtp_message_t message = smtp_add_message(session);
+			auto server = props->getPropertyWithDefault("GentooBrowseAPI.MailServer", "localhost");
+			smtp_set_server(session, server.c_str());
+			smtp_set_header(message, "To", msg->to.name.c_str(), msg->to.address.c_str());
+			smtp_set_header(message, "From", msg->from.name.c_str(), msg->from.address.c_str());
+			smtp_set_header(message, "Subject", msg->subject.c_str());
+			Parts parts;
+			parts.emplace_back("Content-Type: multipart/alternative; boundary=\"<<divider>>\"\r\n");
+			parts.emplace_back("MIME-Version: 1.0\r\n");
+			parts.emplace_back("Content-Transfer-Encoding: binary\r\n");
+			smtp_add_recipient(message, msg->to.address.c_str());
+			for (const auto & p : msg->body) {
+				parts.emplace_back("\r\n--<<divider>>\r\nContent-Type: " + p->mimetype  + "; utf-8\r\n\r\n");
+				parts.emplace_back(p->payload);
+			}
+			parts.emplace_back("\r\n--<<divider>>--\r\n");
+			PartsProgress pp { parts.begin(), parts.end() };
+			smtp_set_messagecb(message, writeBody, &pp);
+			if (!smtp_start_session(session)) {
+				char buf[BUFSIZ];
+				auto b = smtp_strerror(smtp_errno(), buf, sizeof(buf));
+				assert(b);
+				SendEmailFailed e(b);
+				smtp_destroy_session(session);
+				throw e;
+			}
+			smtp_destroy_session(session);
+		}
+
+		const char *
+		MailServer::writeBody(void **, int * len, void * arg)
+		{
+			auto parts = static_cast<PartsProgress *>(arg);
+			if (len == NULL || parts->first == parts->second) {
+				return NULL;
+			}
+			const auto & p = *parts->first++;
+			*len = p.length();
+			return p.data();
+		}
+	}
+
+	void
+	SendEmailFailed::ice_print(std::ostream & buf) const
+	{
+		buf << "Failed to send email: " << message;
+	}
+}
+
diff --git a/gentoobrowse-api/service/notifications/mailserverimpl.h b/gentoobrowse-api/service/notifications/mailserverimpl.h
new file mode 100644
index 0000000..23b17c2
--- /dev/null
+++ b/gentoobrowse-api/service/notifications/mailserverimpl.h
@@ -0,0 +1,20 @@
+#ifndef MAILSERVERIMPL_H
+#define MAILSERVERIMPL_H
+
+#include <notifications.h>
+#include <visibility.h>
+
+namespace Gentoo {
+	namespace Service {
+		class DLL_PUBLIC MailServer : public Gentoo::MailServer {
+			public:
+				void sendEmail(const Gentoo::EmailPtr & msg, const Ice::Current &) override;
+
+			private:
+				static const char * writeBody(void ** buf, int * len, void * arg);
+		};
+	}
+}
+
+#endif
+
diff --git a/gentoobrowse-api/service/notifications/notifications.ice b/gentoobrowse-api/service/notifications/notifications.ice
new file mode 100644
index 0000000..a60fa28
--- /dev/null
+++ b/gentoobrowse-api/service/notifications/notifications.ice
@@ -0,0 +1,49 @@
+#ifndef GENTOO_NOTIFICATIONS
+#define GENTOO_NOTIFICATIONS
+
+#include <user-models.ice>
+#include <portage-models.ice>
+
+module Gentoo {
+	class MimePart {
+		string mimetype;
+		string payload;
+	};
+
+	sequence<MimePart> MimeParts;
+
+	struct Address {
+		string name;
+		string address;
+	};
+
+	class Email {
+		Address to;
+		Address from;
+		string subject;
+		MimeParts body;
+	};
+
+	["cpp:ice_print"]
+	exception SendEmailFailed {
+		string message;
+	};
+
+	struct NewsContent {
+		Categories categories;
+		Packages packages;
+		Ebuilds ebuilds;
+	};
+
+	interface Notifications {
+		Email getSignup(NewUser user);
+		Email getNews(User user, NewsContent nc);
+	};
+
+	interface MailServer {
+		idempotent void sendEmail(Email msg) throws SendEmailFailed;
+	};
+};
+
+#endif
+
diff --git a/gentoobrowse-api/service/notifications/notificationsimpl.cpp b/gentoobrowse-api/service/notifications/notificationsimpl.cpp
new file mode 100644
index 0000000..d7dbee4
--- /dev/null
+++ b/gentoobrowse-api/service/notifications/notificationsimpl.cpp
@@ -0,0 +1,59 @@
+#include "notificationsimpl.h"
+#include <libxml/parser.h>
+#include <base-xslt.h>
+#include <news-xslt.h>
+#include <signup-xslt.h>
+#include <string.h>
+#include <slicer/slicer.h>
+#include "xsltStreamSerializer.h"
+
+namespace Gentoo {
+	namespace Service {
+		xmlDocPtr	xsltDocLoaderFunc	(const xmlChar * URI, xmlDictPtr, int, void *, xsltLoadType)
+		{
+#define MATCH(name) \
+			if (xmlStrcmp(URI, BAD_CAST #name ".xslt") == 0) { \
+				return xmlParseMemory((char *)name ## _xslt, name ## _xslt_len); \
+			}
+			MATCH(base)
+			MATCH(news)
+			MATCH(signup)
+			return NULL;
+#undef MATCH
+		}
+
+		Notifications::Notifications()
+		{
+			xsltSetLoaderFunc(&xsltDocLoaderFunc);
+			news = xsltSSPtr(xsltParseStylesheetFile(BAD_CAST "news.xslt"), xsltFreeStylesheet);
+			signup = xsltSSPtr(xsltParseStylesheetFile(BAD_CAST "signup.xslt"), xsltFreeStylesheet);
+			xsltSetLoaderFunc(NULL);
+		}
+
+		Gentoo::EmailPtr Notifications::getSignup(const Gentoo::NewUserPtr & u, const Ice::Current &)
+		{
+			auto e = basicMail("Welcome", u);
+			Slicer::SerializeAny<XsltStreamSerializer>(u, e, signup.get());
+			return e;
+		}
+
+		Gentoo::EmailPtr Notifications::getNews(const Gentoo::UserPtr & u, const Gentoo::NewsContent & c, const Ice::Current &)
+		{
+			auto e = basicMail("Latest updates", u);
+			Slicer::SerializeAny<XsltStreamSerializer>(c, e, news.get());
+			return e;
+		}
+
+		Gentoo::EmailPtr Notifications::basicMail(const std::string & subject, Gentoo::UserPtr u)
+		{
+			Gentoo::EmailPtr e = new Gentoo::Email();
+			e->subject = "Gentoo Browse: " + subject;
+			e->from.name = "Gentoo Browse";
+			e->from.address = "noreply@gentoobrowse.randomdan.homeip.net";
+			e->to.name = u->userrealname;
+			e->to.address = u->useremail;
+			return e;
+		}
+	}
+}
+
diff --git a/gentoobrowse-api/service/notifications/notificationsimpl.h b/gentoobrowse-api/service/notifications/notificationsimpl.h
new file mode 100644
index 0000000..1d3b1fc
--- /dev/null
+++ b/gentoobrowse-api/service/notifications/notificationsimpl.h
@@ -0,0 +1,29 @@
+#ifndef NOTIFICATIONSIMPL_H
+#define NOTIFICATIONSIMPL_H
+
+#include <notifications.h>
+#include <visibility.h>
+#include <memory>
+#include <libxslt/documents.h>
+
+namespace Gentoo {
+	namespace Service {
+		class DLL_PUBLIC Notifications : public Gentoo::Notifications {
+			public:
+				Notifications();
+
+				Gentoo::EmailPtr getSignup(const Gentoo::NewUserPtr &, const Ice::Current &) override;
+				Gentoo::EmailPtr getNews(const Gentoo::UserPtr &, const Gentoo::NewsContent &, const Ice::Current &) override;
+
+			private:
+				Gentoo::EmailPtr basicMail(const std::string &, Gentoo::UserPtr u);
+
+				typedef std::shared_ptr<xsltStylesheet> xsltSSPtr;
+				xsltSSPtr news;
+				xsltSSPtr signup;
+		};
+	}
+}
+
+#endif
+
diff --git a/gentoobrowse-api/service/notifications/xslt/base.xslt b/gentoobrowse-api/service/notifications/xslt/base.xslt
new file mode 100644
index 0000000..9db7752
--- /dev/null
+++ b/gentoobrowse-api/service/notifications/xslt/base.xslt
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+		xmlns:project2="http://project2.randomdan.homeip.net" exclude-result-prefixes="project2" >
+  <xsl:output encoding="utf-8" method="html" media-type="text/html" indent="yes" />
+
+  <xsl:template match="/*">
+		<xsl:text disable-output-escaping="yes">&lt;!DOCTYPE html&gt;&#10;</xsl:text>
+		<html lang="en">
+			<head>
+				<meta charset="utf-8" />
+				<link href="http://gentoobrowse.randomdan.homeip.net/css/bootstrap.min.css" rel="stylesheet" media="screen" />
+				<link href="http://gentoobrowse.randomdan.homeip.net/css/bootstrap-responsive.min.css" rel="stylesheet" media="screen" />
+				<link href="http://gentoobrowse.randomdan.homeip.net/css/general.css" rel="stylesheet" media="screen" />
+				<title><xsl:call-template name="title" /></title>
+			</head>
+			<body data-spy="scroll" data-target=".bs-docs-sidebar">
+				<!-- nav -->
+				<nav class="navbar navbar-inverse navbar-fixed-top">
+					<div class="navbar-inner">
+						<div class="container">
+							<a class="brand" href="http://gentoobrowse.randomdan.homeip.net/">Gentoo Browse</a>
+						</div>
+					</div>
+				</nav>
+
+				<!-- page content -->
+				<header class="jumbotron subhead">
+					<div class="container">
+						<h1>
+							<xsl:call-template name="title" />
+						</h1>
+					</div>
+				</header>
+				<div class="container">
+					<article class="row">
+						<xsl:call-template name="content" />
+						<section class="signoff">
+						</section>
+					</article>
+				</div>
+
+				<!-- footer -->
+				<footer class="footer">
+					<div class="container">
+						<p>Gentoo Browse is not an official Gentoo website. The name "Gentoo" and the "g" logo are trademarks of the Gentoo Foundation, Inc.</p>
+					</div>
+				</footer>
+
+				<script src="http://code.jquery.com/jquery-latest.js"></script>
+				<script src="http://gentoobrowse.randomdan.homeip.net/js/bootstrap.min.js"></script>
+				<script src="http://gentoobrowse.randomdan.homeip.net/js/js.js"></script>
+			</body>
+		</html>
+  </xsl:template>
+
+  <xsl:template name="head">
+  </xsl:template>
+
+  <xsl:template name="header">
+  </xsl:template>
+</xsl:stylesheet>
diff --git a/gentoobrowse-api/service/notifications/xslt/news.xslt b/gentoobrowse-api/service/notifications/xslt/news.xslt
new file mode 100644
index 0000000..6b2ea29
--- /dev/null
+++ b/gentoobrowse-api/service/notifications/xslt/news.xslt
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fn="http://exslt.org/strings" xmlns:project2="http://project2.randomdan.homeip.net" version="1.0" exclude-result-prefixes="project2 fn">
+  <xsl:import href="base.xslt"/>
+  <xsl:output encoding="utf-8" method="html" media-type="text/html" indent="yes"/>
+
+  <xsl:template name="title">Latest news</xsl:template>
+
+	<xsl:template name="content">
+		<xsl:variable name="packages" select="packages/package[packageid = ../../ebuilds/ebuild/packageid]"/>
+		<xsl:variable name="categories" select="categories/category[categoryid = $packages/categoryid]"/>
+		<section>
+			<h2 class="page-header">New versions of tracked packages</h2>
+			<div class="span3 bs-docs-sidebar">
+				<ul class="nav nav-list bs-docs-sidenav">
+					<xsl:for-each select="$categories">
+						<xsl:sort select="name" data-type="text" order="ascending"/>
+						<li>
+							<a>
+								<xsl:attribute name="href">http://gentoobrowse.randomdan.homeip.net/packages/<xsl:value-of select="name" /></xsl:attribute>
+								<i class="icon-chevron-right"></i>
+								<xsl:value-of select="name" />
+							</a>
+						</li>
+					</xsl:for-each>
+				</ul>
+			</div>
+			<div class="span9">
+				<xsl:for-each select="$categories">
+					<xsl:sort select="name" data-type="text" order="ascending"/>
+					<xsl:variable name="category" select="." />
+					<xsl:for-each select="$packages[categoryid = current()/categoryid]">
+						<xsl:sort select="name" data-type="text" order="ascending"/>
+						<xsl:variable name="package" select="." />
+						<p>
+							<a>
+								<xsl:attribute name="href">http://gentoobrowse.randomdan.homeip.net/packages/<xsl:value-of select="$category/name" /></xsl:attribute>
+								<xsl:value-of select="$category/name" />
+							</a>
+							/
+							<a>
+								<xsl:attribute name="href">http://gentoobrowse.randomdan.homeip.net/packages/<xsl:value-of select="$category/name" />/<xsl:value-of select="name" /></xsl:attribute>
+								<xsl:value-of select="name" />
+							</a>
+							:
+							<xsl:value-of select="description" />
+						</p>
+						<xsl:for-each select="../../ebuilds/ebuild[packageid = current()/packageid]">
+							<p>
+								v<xsl:value-of select="version" />
+							</p>
+						</xsl:for-each>
+					</xsl:for-each>
+				</xsl:for-each>
+			</div>
+		</section>
+	</xsl:template>
+</xsl:stylesheet>
diff --git a/gentoobrowse-api/service/notifications/xslt/signup.xslt b/gentoobrowse-api/service/notifications/xslt/signup.xslt
new file mode 100644
index 0000000..e579835
--- /dev/null
+++ b/gentoobrowse-api/service/notifications/xslt/signup.xslt
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+	<xsl:import href="base.xslt"/>
+
+  <xsl:template name="title">Welcome to Gentoo Browse</xsl:template>
+
+  <xsl:template name="content">
+		<section>
+			<h2 class="page-header">Confirmation</h2>
+			<p>
+				Click <a>
+					<xsl:attribute name="href">
+						<xsl:text>http://gentoobrowse.randomdan.homeip.net/user/verify/</xsl:text>
+						<xsl:value-of select="verifyguid" />
+					</xsl:attribute>
+					<xsl:text>http://gentoobrowse.randomdan.homeip.net/user/verify/</xsl:text>
+					<xsl:value-of select="verifyguid" />
+				</a> to confirm your account.
+			</p>
+			<p>Or alternatively, copy and paste this code into the confirmation page: <xsl:value-of select="verifyguid" /></p>
+		</section>
+  </xsl:template>
+</xsl:stylesheet>
+
diff --git a/gentoobrowse-api/service/notifications/xsltStreamSerializer.cpp b/gentoobrowse-api/service/notifications/xsltStreamSerializer.cpp
new file mode 100644
index 0000000..5a71b35
--- /dev/null
+++ b/gentoobrowse-api/service/notifications/xsltStreamSerializer.cpp
@@ -0,0 +1,82 @@
+#include "xsltStreamSerializer.h"
+#include <libxslt/xsltInternals.h>
+#include <libxml/HTMLtree.h>
+#include <factory.impl.h>
+#include <processPipes.h>
+#include <sys/wait.h>
+
+namespace Gentoo {
+	static int xmlstrmclosecallback(void * context)
+	{
+		((std::ostream*)context)->flush();
+		return 0;
+	}
+
+	static int xmlstrmwritecallback(void * context, const char * buffer, int len)
+	{
+		((std::ostream*)context)->write(buffer, len);
+		return len;
+	}
+
+	XsltStreamSerializer::XsltStreamSerializer(Gentoo::EmailPtr e, xsltStylesheet * ss) :
+		Slicer::XmlDocumentSerializer(doc),
+		mail(e),
+		doc(nullptr),
+		stylesheet(ss)
+	{
+	}
+
+	XsltStreamSerializer::~XsltStreamSerializer()
+	{
+		delete doc;
+	}
+
+	void
+	XsltStreamSerializer::Serialize(Slicer::ModelPartForRootPtr mp)
+	{
+		Slicer::XmlDocumentSerializer::Serialize(mp);
+		auto result = std::shared_ptr<xmlDoc>(xsltApplyStylesheet(stylesheet, doc->cobj(), nullptr), xmlFreeDoc);
+		if (!result) {
+			throw xmlpp::exception("Failed to apply XSL transform");
+		}
+		appendText(result.get());
+		appendHtml(result.get());
+	}
+
+	void XsltStreamSerializer::appendHtml(xmlDoc * result) const
+	{
+		std::stringstream strm;
+		xmlOutputBufferPtr buf = xmlOutputBufferCreateIO(xmlstrmwritecallback, xmlstrmclosecallback, &strm, NULL);
+		htmlDocContentDumpFormatOutput(buf, result, "utf-8", 0);
+		xmlOutputBufferClose(buf);
+		mail->body.push_back(new Gentoo::MimePart("text/html", strm.str()));
+	}
+
+	void XsltStreamSerializer::appendText(xmlDoc * result) const
+	{
+		std::stringstream strm;
+		std::vector<std::string> callLynx;
+		callLynx.push_back("/usr/bin/lynx");
+		callLynx.push_back("-dump");
+		callLynx.push_back("-stdin");
+		std::string widthArg = "-width=105";
+		callLynx.push_back(widthArg);
+		AdHoc::System::ProcessPipes fds(callLynx, true, true, false);
+		FILE * lynxIn = fdopen(fds.fdIn(), "w");
+		// Fixed encoding as we want the result to go back into a ustring
+		htmlNodeDumpFileFormat(lynxIn, result, xmlDocGetRootElement(result), "utf-8", 0);
+		fclose(lynxIn);
+		char buf[1024];
+		int r;
+		while ((r = read(fds.fdOut(), buf, sizeof(buf))) > 0) {
+			strm.write(buf, r);
+		}
+		int status;
+		waitpid(fds.pid(), &status, 0);
+		if (status != 0) {
+			throw std::runtime_error("Lynx failed");
+		}
+		mail->body.push_back(new Gentoo::MimePart("text/plain", strm.str()));
+	}
+}
+
diff --git a/gentoobrowse-api/service/notifications/xsltStreamSerializer.h b/gentoobrowse-api/service/notifications/xsltStreamSerializer.h
new file mode 100644
index 0000000..492b954
--- /dev/null
+++ b/gentoobrowse-api/service/notifications/xsltStreamSerializer.h
@@ -0,0 +1,28 @@
+#ifndef ICESPIDER_CORE_XSLTSTREAMSERIALIZER_H
+#define ICESPIDER_CORE_XSLTSTREAMSERIALIZER_H
+
+#include <slicer/xml/serializer.h>
+#include <visibility.h>
+#include <libxslt/transform.h>
+#include <notifications.h>
+
+namespace Gentoo {
+	class DLL_PUBLIC XsltStreamSerializer : public Slicer::XmlDocumentSerializer {
+		public:
+			XsltStreamSerializer(Gentoo::EmailPtr, xsltStylesheet *);
+			~XsltStreamSerializer();
+
+			void Serialize(Slicer::ModelPartForRootPtr mp) override;
+
+		protected:
+			void appendHtml(xmlDoc *) const;
+			void appendText(xmlDoc *) const;
+
+			Gentoo::EmailPtr mail;
+			xmlpp::Document * doc;
+			xsltStylesheet * stylesheet;
+	};
+}
+
+#endif
+
diff --git a/gentoobrowse-api/unittests/Jamfile.jam b/gentoobrowse-api/unittests/Jamfile.jam
index 92501c0..70fa90f 100644
--- a/gentoobrowse-api/unittests/Jamfile.jam
+++ b/gentoobrowse-api/unittests/Jamfile.jam
@@ -97,6 +97,14 @@ run
 	<library>testCommon
 	: testUsers ;
 
+run
+	testNotifications.cpp
+	: : :
+	<dependency>../db/schema.sql
+	<define>BOOST_TEST_DYN_LINK
+	<library>testCommon
+	: testNotifications ;
+
 run
 	testBugs.cpp
 	: : :
diff --git a/gentoobrowse-api/unittests/mockDefs.cpp b/gentoobrowse-api/unittests/mockDefs.cpp
index 70f5b69..eeab5f2 100644
--- a/gentoobrowse-api/unittests/mockDefs.cpp
+++ b/gentoobrowse-api/unittests/mockDefs.cpp
@@ -2,6 +2,9 @@
 #include <definedDirs.h>
 
 Service::Service() :
+	IceTray::DryIce({
+			"--GentooBrowseAPI.MailServer=smtp.random.lan:25",
+		}),
 	PQ::Mock("user=postgres dbname=postgres", "GentooBrowseAPI", {
 			rootDir.parent_path() / "db" / "schema.sql",
 			rootDir / "data.sql" })
@@ -22,6 +25,8 @@ Maintenance::Maintenance() :
 TestClient::TestClient() :
 	m(getProxy<Gentoo::MaintenancePrx>("maintenance")),
 	p(getProxy<Gentoo::PortagePrx>("portage")),
+	n(getProxy<Gentoo::NotificationsPrx>("notifications")),
+	ms(getProxy<Gentoo::MailServerPrx>("mailserver")),
 	u(getProxy<Gentoo::UsersPrx>("users"))
 {
 }
diff --git a/gentoobrowse-api/unittests/mockDefs.h b/gentoobrowse-api/unittests/mockDefs.h
index 9a35c1f..a0c044a 100644
--- a/gentoobrowse-api/unittests/mockDefs.h
+++ b/gentoobrowse-api/unittests/mockDefs.h
@@ -7,6 +7,7 @@
 #include <portage.h>
 #include <users.h>
 #include <maintenance.h>
+#include <notifications.h>
 #include <selectcommandUtil.impl.h>
 
 class DLL_PUBLIC Service : public IceTray::DryIce, PQ::Mock {
@@ -25,6 +26,8 @@ class DLL_PUBLIC TestClient : public IceTray::DryIceClient {
 
 		Gentoo::MaintenancePrx m;
 		Gentoo::PortagePrx p;
+		Gentoo::NotificationsPrx n;
+		Gentoo::MailServerPrx ms;
 		Gentoo::UsersPrx u;
 };
 
diff --git a/gentoobrowse-api/unittests/testNotifications.cpp b/gentoobrowse-api/unittests/testNotifications.cpp
new file mode 100644
index 0000000..81b28d0
--- /dev/null
+++ b/gentoobrowse-api/unittests/testNotifications.cpp
@@ -0,0 +1,102 @@
+#define BOOST_TEST_MODULE TestNotifications
+#include <boost/test/unit_test.hpp>
+
+#include "mockDefs.h"
+#include <fstream>
+
+BOOST_GLOBAL_FIXTURE( Service );
+
+BOOST_FIXTURE_TEST_SUITE(tp, TestClient);
+
+bool
+isHtml(const std::string & body)
+{
+	return body.find("<html") != std::string::npos;
+}
+
+void
+save(const std::string & name, Gentoo::EmailPtr e)
+{
+	std::ofstream text("/tmp/" + name + ".txt");
+	text << e->body[0]->payload;
+	std::ofstream html("/tmp/" + name + ".html");
+	html << e->body[1]->payload;
+}
+
+void
+commonAssert(Gentoo::EmailPtr e)
+{
+	BOOST_REQUIRE_EQUAL(e->body.size(), 2);
+	BOOST_REQUIRE_EQUAL(e->body[0]->mimetype, "text/plain");
+	BOOST_REQUIRE(!isHtml(e->body[0]->payload));
+	BOOST_REQUIRE_EQUAL(e->body[1]->mimetype, "text/html");
+	BOOST_REQUIRE(isHtml(e->body[1]->payload));
+}
+
+#if 0
+BOOST_AUTO_TEST_CASE( testSend )
+{
+	Gentoo::NewUserPtr u = new Gentoo::NewUser(1, "testuser", "Test User", "dangoodliffe@gmail.com", "some-guid");
+	auto e = n->getSignup(u);
+	BOOST_REQUIRE(e);
+	ms->sendEmail(e);
+}
+#endif
+
+BOOST_AUTO_TEST_CASE( testSignup )
+{
+	Gentoo::NewUserPtr u = new Gentoo::NewUser(1, "testuser", "Test User", "test@user.com", "some-guid");
+	auto e = n->getSignup(u);
+	BOOST_REQUIRE(e);
+	BOOST_REQUIRE_EQUAL(e->subject, "Gentoo Browse: Welcome");
+	BOOST_REQUIRE_EQUAL(e->from.name, "Gentoo Browse");
+	BOOST_REQUIRE_EQUAL(e->from.address, "noreply@gentoobrowse.randomdan.homeip.net");
+	BOOST_REQUIRE_EQUAL(e->to.name, "Test User");
+	BOOST_REQUIRE_EQUAL(e->to.address, "test@user.com");
+	commonAssert(e);
+	save("signup", e);
+	for (auto p : e->body) {
+		BOOST_REQUIRE(p->payload.find("Welcome to Gentoo Browse") != std::string::npos);
+		BOOST_REQUIRE(p->payload.find("http://gentoobrowse.randomdan.homeip.net/user/verify/some-guid") != std::string::npos);
+		BOOST_REQUIRE(p->payload.find("confirmation page: some-guid") != std::string::npos);
+	}
+}
+
+BOOST_AUTO_TEST_CASE( testNews )
+{
+	Gentoo::UserPtr u = new Gentoo::User(1, "testuser", "Test User", "test@user.com");
+	Gentoo::NewsContent nc {
+		{
+			new Gentoo::Category(1, "app-test", "Test cat1"),
+			new Gentoo::Category(2, "sys-test", "Test cat2"),
+			new Gentoo::Category(3, "no-show", "Unused cat")
+		},
+		{
+			new Gentoo::Package(1, 1, "app1", "first-seen", "Test app desc one", "Test summary 1", IceUtil::None, IceUtil::None, IceUtil::None, IceUtil::None),
+			new Gentoo::Package(2, 1, "app2", "first-seen", "Test app desc two", "Test summary 1", IceUtil::None, IceUtil::None, IceUtil::None, IceUtil::None),
+			new Gentoo::Package(3, 2, "app3", "first-seen", "Test app desc three", "Test summary 1", IceUtil::None, IceUtil::None, IceUtil::None, IceUtil::None),
+			new Gentoo::Package(4, 2, "app4", "first-seen", "Test app no-show", "Test summary 1", IceUtil::None, IceUtil::None, IceUtil::None, IceUtil::None)
+		},
+		{
+			new Gentoo::Ebuild(1, 1, 1, "1.0a", "slot", "first-seen", "last-mod", IceUtil::None),
+			new Gentoo::Ebuild(2, 1, 1, "1.1a", "slot", "first-seen", "last-mod", IceUtil::None),
+			new Gentoo::Ebuild(3, 1, 1, "1.2a", "slot", "first-seen", "last-mod", IceUtil::None),
+			new Gentoo::Ebuild(4, 2, 1, "2.0a", "slot", "first-seen", "last-mod", IceUtil::None),
+			new Gentoo::Ebuild(5, 3, 1, "3.0a", "slot", "first-seen", "last-mod", IceUtil::None),
+			new Gentoo::Ebuild(6, 2, 1, "2.1a", "slot", "first-seen", "last-mod", IceUtil::None),
+			new Gentoo::Ebuild(7, 3, 1, "3.1a", "slot", "first-seen", "last-mod", IceUtil::None)
+		}
+	};
+	auto e = n->getNews(u, nc);
+	commonAssert(e);
+	save("news", e);
+	for (auto p : e->body) {
+		BOOST_REQUIRE(p->payload.find("Latest news") != std::string::npos);
+		// BOOST_REQUIRE(p->payload.find("http://gentoobrowse.randomdan.homeip.net/packages/app-test") != std::string::npos);
+		// BOOST_REQUIRE(p->payload.find("http://gentoobrowse.randomdan.homeip.net/packages/app-test/app") != std::string::npos);
+		BOOST_REQUIRE(p->payload.find("no-show") == std::string::npos);
+	}
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
-- 
cgit v1.2.3