Implement vote closing, refactor notifications
authorJan Dittberner <jandd@cacert.org>
Wed, 19 Apr 2017 21:32:12 +0000 (23:32 +0200)
committerJan Dittberner <jan@dittberner.info>
Fri, 21 Apr 2017 22:12:38 +0000 (00:12 +0200)
actions.go [deleted file]
boardvoting.go
denied.php [deleted file]
index.php [deleted file]
models.go
motion.php [deleted file]
motions.php [deleted file]
notifications.go [new file with mode: 0644]

diff --git a/actions.go b/actions.go
deleted file mode 100644 (file)
index 19e9bf3..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
-package main
-
-import (
-       "bytes"
-       "fmt"
-       "github.com/Masterminds/sprig"
-       "gopkg.in/gomail.v2"
-       "text/template"
-)
-
-type templateBody string
-
-func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer, err error) {
-       t, err := template.New(templateName).Funcs(
-               sprig.GenericFuncMap()).ParseFiles(fmt.Sprintf("templates/%s", templateName))
-       if err != nil {
-               return
-       }
-
-       mailText = bytes.NewBufferString("")
-       t.Execute(mailText, context)
-
-       return
-}
-
-func CreateMotion(decision *Decision, voter *Voter) (err error) {
-       decision.ProponentId = voter.Id
-       err = decision.Create()
-       if err != nil {
-               logger.Println("Error saving motion:", err)
-               return
-       }
-
-       type mailContext struct {
-               Decision
-               Name       string
-               VoteURL    string
-               UnvotedURL string
-       }
-       voteURL := fmt.Sprintf("%s/vote", config.BaseURL)
-       unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
-       context := mailContext{*decision, voter.Name, voteURL, unvotedURL}
-
-       mailText, err := buildMail("create_motion_mail.txt", context)
-       if err != nil {
-               logger.Println("Error", err)
-               return
-       }
-
-       m := gomail.NewMessage()
-       m.SetHeader("From", config.NoticeSenderAddress)
-       m.SetHeader("To", config.BoardMailAddress)
-       m.SetHeader("Subject", fmt.Sprintf("%s - %s", decision.Tag, decision.Title))
-       m.SetHeader("Message-ID", fmt.Sprintf("<%s>", decision.Tag))
-       m.SetBody("text/plain", mailText.String())
-
-       d := gomail.NewDialer(config.MailServer.Host, config.MailServer.Port, "", "")
-       if err := d.DialAndSend(m); err != nil {
-               logger.Println("Error sending mail", err)
-       }
-
-       return
-}
-
-func UpdateMotion(decision *Decision, voter *Voter) (err error) {
-       err = decision.Update()
-       if err != nil {
-               logger.Println("Error updating motion:", err)
-               return
-       }
-
-       type mailContext struct {
-               Decision
-               Name       string
-               VoteURL    string
-               UnvotedURL string
-       }
-       voteURL := fmt.Sprintf("%s/vote", config.BaseURL)
-       unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
-       context := mailContext{*decision, voter.Name, voteURL, unvotedURL}
-
-       mailText, err := buildMail("update_motion_mail.txt", context)
-       if err != nil {
-               logger.Println("Error", err)
-               return
-       }
-
-       m := gomail.NewMessage()
-       m.SetHeader("From", config.NoticeSenderAddress)
-       m.SetHeader("To", config.BoardMailAddress)
-       m.SetHeader("Subject", fmt.Sprintf("Re: %s - %s", decision.Tag, decision.Title))
-       m.SetHeader("References", fmt.Sprintf("<%s>", decision.Tag))
-       m.SetBody("text/plain", mailText.String())
-
-       d := gomail.NewDialer(config.MailServer.Host, config.MailServer.Port, "", "")
-       if err := d.DialAndSend(m); err != nil {
-               logger.Println("Error sending mail", err)
-       }
-
-       return
-}
-
-func WithdrawMotion(decision *Decision, voter *Voter) (err error) {
-       err = decision.UpdateStatus()
-
-       type mailContext struct {
-               *Decision
-               Name string
-       }
-       context := mailContext{decision, voter.Name}
-
-       mailText, err := buildMail("withdraw_motion_mail.txt", context)
-       if err != nil {
-               logger.Println("Error", err)
-               return
-       }
-
-       m := gomail.NewMessage()
-       m.SetHeader("From", config.NoticeSenderAddress)
-       m.SetHeader("To", config.BoardMailAddress)
-       m.SetHeader("Subject", fmt.Sprintf("Re: %s - %s - withdrawn", decision.Tag, decision.Title))
-       m.SetHeader("References", fmt.Sprintf("<%s>", decision.Tag))
-       m.SetBody("text/plain", mailText.String())
-
-       d := gomail.NewDialer(config.MailServer.Host, config.MailServer.Port, "", "")
-       if err := d.DialAndSend(m); err != nil {
-               logger.Println("Error sending mail", err)
-       }
-
-       return
-}
index 187a867..30de17e 100644 (file)
@@ -271,16 +271,20 @@ func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
        case http.MethodPost:
                decision.Status = voteStatusWithdrawn
                decision.Modified = time.Now().UTC()
-               if err := WithdrawMotion(&decision.Decision, voter); err != nil {
+               if err := decision.UpdateStatus(); err != nil {
+                       logger.Println("Error withdrawing motion:", err)
                        http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                        return
                }
+
+               notifyMail <- &NotificationWithDrawMotion{decision: decision.Decision, voter: *voter}
+
                if err := a.AddFlash(w, r, fmt.Sprintf("Motion %s has been withdrawn!", decision.Tag)); err != nil {
                        http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                        return
                }
+
                http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect)
-               return
        default:
                templateContext.Decision = decision
                renderTemplate(w, templates, templateContext)
@@ -319,10 +323,15 @@ func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
                        renderTemplate(w, templates, templateContext)
                } else {
                        data.Proposed = time.Now().UTC()
-                       if err := CreateMotion(data, voter); err != nil {
+                       data.ProponentId = voter.Id
+                       if err := data.Create(); err != nil {
+                               logger.Println("Error saving motion:", err)
                                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                                return
                        }
+
+                       notifyMail <- &NotificationCreateMotion{decision: *data, voter: *voter}
+
                        if err := h.AddFlash(w, r, "The motion has been proposed!"); err != nil {
                                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                                return
@@ -380,10 +389,14 @@ func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
                        renderTemplate(w, templates, templateContext)
                } else {
                        data.Modified = time.Now().UTC()
-                       if err := UpdateMotion(data, voter); err != nil {
+                       if err := data.Update(); err != nil {
+                               logger.Println("Error updating motion:", err)
                                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                                return
                        }
+
+                       notifyMail <- &NotificationUpdateMotion{decision: *data, voter: *voter}
+
                        if err := a.AddFlash(w, r, "The motion has been modified!"); err != nil {
                                http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
                                return
@@ -502,6 +515,9 @@ func main() {
 
        defer db.Close()
 
+       go MailNotifier()
+       defer CloseMailNotifier()
+
        http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
        http.Handle("/newmotion/", motionsHandler{})
        http.Handle("/static/", http.FileServer(http.Dir(".")))
diff --git a/denied.php b/denied.php
deleted file mode 100644 (file)
index 9bb72d6..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-<html>
-       <head>
-               <title>CAcert Board Decisions</title>
-               <meta http-equiv="Content-Type" content="text/html; charset='UTF-8'" />
-               <link rel="stylesheet" type="text/css" href="styles.css" />
-       </head>
-       <body>
-               <b>You are not authorized to act here!</b><br/>
-               <i>If you think this is in error, please contact the administrator</i>
-               <i>If you don't know who that is, it is definitely not an error ;)</i>
-       </body>
-</html>
\ No newline at end of file
diff --git a/index.php b/index.php
deleted file mode 100644 (file)
index 3363496..0000000
--- a/index.php
+++ /dev/null
@@ -1,5 +0,0 @@
-<?php
-       header("HTTP/1.0 301 Redirect");
-       header("Location: motions.php");
-       exit();
-?>
\ No newline at end of file
index d6eee15..5df7d95 100644 (file)
--- a/models.go
+++ b/models.go
@@ -21,6 +21,7 @@ const (
        sqlCreateDecision
        sqlUpdateDecision
        sqlUpdateDecisionStatus
+       sqlSelectClosableDecisions
 )
 
 var sqlStatements = map[sqlKey]string{
@@ -103,6 +104,12 @@ WHERE id=:id`,
 UPDATE decisions
 SET status=:status, modified=:modified WHERE id=:id
 `,
+       sqlSelectClosableDecisions: `
+SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title,
+       decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified
+FROM   decisions
+JOIN   voters ON decisions.proponent=voters.id
+WHERE  decisions.status=0 AND :now > due`,
 }
 
 var db *sqlx.DB
@@ -506,3 +513,81 @@ func FindVoterByAddress(emailAddress string) (voter *Voter, err error) {
        }
        return
 }
+
+func (d *Decision) Close() (err error) {
+       closeDecisionStmt, err := db.PrepareNamed(sqlStatements[sqlUpdateDecisionStatus])
+       if err != nil {
+               logger.Println("Error preparing statement:", err)
+               return
+       }
+       defer closeDecisionStmt.Close()
+
+       quorum, majority := d.VoteType.QuorumAndMajority()
+
+       voteSums, err := d.VoteSums()
+
+       if err != nil {
+               logger.Println("Error getting vote sums")
+               return
+       }
+       votes := voteSums.VoteCount()
+
+       if votes < quorum {
+               d.Status = voteStatusDeclined
+       } else {
+               votes = voteSums.Ayes + voteSums.Nayes
+               if (voteSums.Ayes / votes) > (majority / 100) {
+                       d.Status = voteStatusApproved
+               } else {
+                       d.Status = voteStatusDeclined
+               }
+       }
+
+       result, err := closeDecisionStmt.Exec(d)
+       if err != nil {
+               logger.Println("Error closing vote:", err)
+               return
+       }
+       affectedRows, err := result.RowsAffected()
+       if err != nil {
+               logger.Println("Error getting affected rows:", err)
+       }
+       if affectedRows != 1 {
+               logger.Printf("WARNING wrong number of affected rows: %d (1 expected)\n", affectedRows)
+       }
+
+       notifyMail <- &NotificationClosedDecision{decision: *d, voteSums: *voteSums}
+
+       return
+}
+
+func CloseDecisions() (err error) {
+       getClosedDecisionsStmt, err := db.PrepareNamed(sqlStatements[sqlSelectClosableDecisions])
+       if err != nil {
+               logger.Println("Error preparing statement:", err)
+               return
+       }
+       defer getClosedDecisionsStmt.Close()
+
+       params := make(map[string]interface{}, 1)
+       params["now"] = time.Now().UTC()
+       rows, err := getClosedDecisionsStmt.Queryx(params)
+       if err != nil {
+               logger.Println("Error fetching closed decisions", err)
+               return
+       }
+       defer rows.Close()
+       for rows.Next() {
+               d := &Decision{}
+               if err = rows.StructScan(d); err != nil {
+                       logger.Println("Error filling decision from database row", err)
+                       return
+               }
+               if err = d.Close(); err != nil {
+                       logger.Printf("Error closing decision %s: %s\n", d.Tag, err)
+                       return
+               }
+       }
+
+       return
+}
diff --git a/motion.php b/motion.php
deleted file mode 100644 (file)
index 2dec354..0000000
+++ /dev/null
@@ -1,198 +0,0 @@
-<?php
-       if ($_SERVER['HTTPS'] != 'on') {
-               header("HTTP/1.0 302 Redirect");
-               header("Location: https://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
-               exit();
-       }
-       require_once("database.php");
-       $db = new DB();
-       if (!($user = $db->auth())) {
-               header("HTTP/1.0 302 Redirect");
-               header("Location: denied.php");
-               exit();
-       }
-       $db->getStatement("stats")->execute();
-       $stats = $db->getStatement("stats")->fetch();
-?>
-<html>
-       <head>
-               <title>CAcert Board Decisions</title>
-               <meta http-equiv="Content-Type" content="text/html; charset='UTF-8'" />
-               <link rel="stylesheet" type="text/css" href="styles.css" />
-       </head>
-       <body>
-               <?php
-               if ($_REQUEST['action'] == "store") {
-                       if (is_numeric($_REQUEST['motion'])) {
-                               $stmt = $db->getStatement("update decision");
-                               $stmt->bindParam(":id",$_POST['motion']);
-                               $stmt->bindParam(":proponent",$user['id']);
-                               $stmt->bindParam(":title",$_POST['title']);
-                               $stmt->bindParam(":content",$_POST['content']);
-                               $stmt->bindParam(":due",$_POST['due']);
-                               $stmt->bindParam(":votetype",$_POST['votetype']);
-                               if ($stmt->execute()) {
-                                       ?>
-                                       <b>The motion has been proposed!</b><br/>
-                                       <a href="motions.php">Back to motions</a><br/>
-                                       <br/>
-                                       <br/>
-                                       <?php
-                                       $decision = $db->getStatement("get decision")->execute(array($_POST['motion']))?$db->getStatement("get decision")->fetch():array();
-                                       $name = $user['name'];
-                                       $tag = $decision['tag'];
-                                       $title = $decision['title'];
-                                       $content =$decision['content'];
-                                       $due = $decision['due']." UTC";
-                                       $votetype = !$decision['votetype'] ? 'motion' : 'veto';
-                                       $baseurl = "https://".$_SERVER['HTTP_HOST'].":".$_SERVER['SERVER_PORT'].preg_replace('/motion\.php/','',$_SERVER['REQUEST_URI']);
-                                       $voteurl = $baseurl."vote.php?motion=".$decision['id'];
-                                       $unvoted = $baseurl."motions.php?unvoted=1";
-                                       $body = <<<BODY
-Dear Board,
-
-$name has modified motion $tag to the following:
-
-$title
-$content
-
-Vote type: $votetype
-
-To vote please choose:
-
-Aye: $voteurl&vote=1
-Naye: $voteurl&vote=-1
-Abstain: $voteurl&vote=0
-
-Please be aware, that if you have voted already your vote is still registered and valid.
-If this modification has an impact on how you wish to vote, you are responsible for voting
-again.
-
-To see all your outstanding votes : $unvoted
-
-Kind regards,
-the voting system
-BODY;
-                                       $db->notify("Re: $tag - $title - modified",$body,$tag);
-                               } else {
-                                       ?>
-                                       <b>The motion has NOT been proposed!</b><br/>
-                                       <a href="motions.php">Back to motions</a><br/>
-                                       <i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i><br/>
-                                       <br/>
-                                       <br/>
-                                       <?php
-                               }
-                       } else {
-                               $stmt = $db->getStatement("create decision");
-                               $stmt->bindParam(":proponent",$user['id']);
-                               $stmt->bindParam(":title",$_POST['title']);
-                               $stmt->bindParam(":content",$_POST['content']);
-                               $stmt->bindParam(":votetype",$_POST['votetype']);
-                               $stmt->bindParam(":due",$_POST['due']);
-                               if ($stmt->execute()) {
-                                       ?>
-                                       <b>The motion has been proposed!</b><br/>
-                                       <a href="motions.php">Back to motions</a><br/>
-                                       <br/>
-                                       <br/>
-                                       <?php
-                                       $decision = $db->getStatement("get new decision")->execute()?$db->getStatement("get new decision")->fetch():array();
-                                       $name = $user['name'];
-                                       $tag = $decision['tag'];
-                                       $title = $decision['title'];
-                                       $content =$decision['content'];
-                                       $due = $decision['due']." UTC";
-                                       $votetype = !$decision['votetype'] ? 'motion' : 'veto';
-                                       $baseurl = "https://".$_SERVER['HTTP_HOST'].":".$_SERVER['SERVER_PORT'].preg_replace('/motion\.php/','',$_SERVER['REQUEST_URI']);
-                                       $voteurl = $baseurl."vote.php?motion=".$decision['id'];
-                                       $unvoted = $baseurl."motions.php?unvoted=1";
-                                       $body = <<<BODY
-Dear Board,
-
-$name has made the following motion:
-
-$title
-$content
-
-Vote type: $votetype
-
-Voting will close $due.
-
-To vote please choose:
-
-Aye: $voteurl&vote=1
-Naye: $voteurl&vote=-1
-Abstain: $voteurl&vote=0
-
-To see all your outstanding votes : $unvoted
-
-Kind regards,
-the voting system
-BODY;
-                                       $db->notify("$tag - $title",$body,$tag,TRUE);
-                               } else {
-                                       ?>
-                                       <b>The motion has NOT been proposed!</b><br/>
-                                       <a href="motions.php">Back to motions</a><br/>
-                                       <i><?php echo join("<br/>\n",$stmt->errorInfo()); ?></i><br/>
-                                       <br/>
-                                       <br/>
-                                       <?php
-                               }
-                       }
-                       
-               }
-               if (is_numeric($_REQUEST['motion'])) {
-                       $stmt = $db->getStatement("get decision");
-                       if ($stmt->execute(array($_REQUEST['motion']))) {
-                               $motion = $stmt->fetch();
-                       }
-                       if (!is_numeric($motion['id'])) {
-                               $motion = array();
-                               foreach (array("title","content") as $column) {
-                                       $motion[$column] = "";
-                               }
-                               $motion["proposer"] = $user['name'];
-                               $motion["votetype"] = 0; // defaults to motion
-                       }
-               } else {
-                       $motion = array();
-                       foreach (array("title","content") as $column) {
-                               $motion[$column] = "";
-                       }
-                       $motion["proposer"] = $user['name'];
-                       $motion["votetype"] = 0; // defaults to motion
-               }
-               ?>
-               <form <?php if (is_numeric($_REQUEST['motion'])) { echo(" action=\"?\""); } ?> method="POST">
-                       <input type="hidden" name="action" value="store" />
-                       <?php
-                               if (is_numeric($_REQUEST['motion'])) {
-                                       ?><input type="hidden" name="motion" value="<?php echo($_REQUEST["motion"]); ?>" /><?php
-                               }
-                       ?>
-               <table>
-                       <tr><td>ID:</td><td><?php echo htmlentities($motion['tag']); ?></td></tr>
-                       <tr><td>Proponent:</td><td><?php echo htmlentities($motion['proposer']); ?></td></tr>
-                       <tr><td>Proposed date/time:</td><td><?php echo htmlentities($motion['proposed'] ? $motion['proposed']." UTC" : '(auto filled to current date/time)'); ?></td></tr>
-                       <tr><td>Title:</td><td><input name="title" value="<?php echo htmlentities($motion['title'])?>"></td></tr>
-                       <tr><td>Text:</td><td><textarea name="content"><?php echo htmlspecialchars($motion['content'])?></textarea></td></tr>
-                       <tr><td>Vote type:</td><td><select name="votetype">
-                               <option value="0" <?php if(!$motion['votetype']) { echo(" selected=\"selected\""); } ?>>Motion</option>
-                               <option value="1" <?php if($motion['votetype']) { echo(" selected=\"selected\""); } ?>>Veto</option>
-                       </select></td></tr>
-                       <tr><td rowspan="2">Due:</td><td><?php echo($motion['due'] ? $motion['due'].' UTC' : '(autofilled from option below)')?></td></tr>
-                       <tr><td><select name="due">
-                               <option value="+3 days">In 3 Days</option>
-                               <option value="+7 days">In 1 Week</option>
-                               <option value="+14 days">In 2 Weeks</option>
-                               <option value="+28 days">In 4 Weeks</option>
-                       </select></td></tr>
-                       <tr><td>&nbsp;</td><td><input type="submit" value="Propose" /></td></tr>
-               </table>
-               </form>
-               <br/>
-               <a href="motions.php">Back to motions</a>               
-       </body>
-</html>
diff --git a/motions.php b/motions.php
deleted file mode 100644 (file)
index 548731f..0000000
+++ /dev/null
@@ -1,167 +0,0 @@
-<?php
-       require_once("database.php");
-       $db = new DB();
-       $page = is_numeric($_REQUEST['page'])?$_REQUEST['page']:1;
-       $user = $db->auth();
-
-       if ($_REQUEST['withdrawl'] && $_REQUEST['confirm'] && $_REQUEST['id']) {
-               if (!$user) {
-                       header("HTTP/1.0 302 Redirect");
-                       header("Location: denied.php");
-                       exit();
-               }
-               $stmt = $db->getStatement("get decision");
-               $stmt->bindParam(":decision",$_REQUEST['id']);
-               if ($stmt->execute() && ($decision=$stmt->fetch())) {
-                       $name = $user['name'];
-                       $tag = $decision['tag'];
-                       $title = $decision['title'];
-                       $content = $decision['content'];
-                       $body = <<<BODY
-Dear Board,
-
-$name has withdrawn the motion $tag that was as follows:
-
-$title
-$content
-
-Kind regards,
-the voting system
-BODY;
-                       $db->notify("Re: $tag - $title - withdrawn",$body,$tag);
-               }
-               $stmt = $db->getStatement("close decision");
-               $status = -2;
-               $stmt->bindParam(":status",$status);
-               $stmt->bindParam(":decision",$_REQUEST['id']);
-               $stmt->execute();
-       }
-?>
-<html>
-       <head>
-               <title>CAcert Board Decisions</title>
-               <meta http-equiv="Content-Type" content="text/html; charset='UTF-8'" />
-               <link rel="stylesheet" type="text/css" href="styles.css" />
-       </head>
-       <body>
-       <?php
-               if ($user) echo '<a href="?unvoted=1">Show my outstanding votes</a><br/>'; 
-       ?>
-               <table class="list">
-                       <tr>
-                               <th>Status</th>
-                               <th>Motion</th>
-                               <th>Actions</th>
-                       </tr>
-                       <?php
-                               if ($_REQUEST['motion']) {
-                                       $stmt = $db->getStatement("list decision");
-                                       $stmt->execute(array($_REQUEST['motion']));
-                               } else {
-                                       if ($user && $_REQUEST['unvoted']) {
-                                               $stmt = $db->getStatement("list my unvoted decisions");
-                                               $stmt->bindParam(":id",$user['id']);
-                                       } else {
-                                               $stmt = $db->getStatement("list decisions");
-                                       }
-                                       $stmt->bindParam(":page",$page);
-                                       $stmt->execute();
-                               }
-                               $items = 0;
-                               $id = -1;
-                               while ($row = $stmt->fetch()) {
-                                       $items++;
-                                       $id = $row['id'];
-                                       ?><tr>
-                                               <td class="<?php switch($row['status']) { case 0: echo "pending"; break; case 1: echo "approved"; break; case -1: echo "declined"; break; case -2: echo "withdrawn"; break; }?>">
-                                               <?php 
-                                                       switch($row['status']) { 
-                                                               case 0: echo "Pending<br/><i>".$row['due']." UTC</i>"; break; 
-                                                               case 1: echo "Approved<br/><i>".$row['modified']." UTC</i>"; break; 
-                                                               case -1: echo "Declined<br/><i>".$row['modified']." UTC</i>"; break; 
-                                                               case -2: echo "Withdrawn<br/><i>".$row['modified']." UTC</i>"; break; 
-                                                       }
-                                               ?>
-                                               </td>
-                                               <td>
-                                               <i><a href="motions.php?motion=<?php echo $row['tag'].'">'.$row['tag']; ?></a></i><br/>
-                                               <b><?php echo htmlspecialchars($row['title']); ?></b><br/>
-                                               <pre><?php echo wordwrap(htmlspecialchars($row['content'])); ?></pre>
-                                               <br/>
-                                               <i>Due: <?php echo($row['due']); ?> UTC</i><br/>
-                                               <i>Proposed: <?php echo($row['proposer']); ?> (<?php echo($row['proposed']); ?> UTC)</i><br/>
-                                               <i>Vote type: <?php echo(!$row['votetype']?'motion':'veto'); ?></i><br/>
-                                               <i>Aye|Naye|Abstain: <?php echo($row['ayes']); ?>|<?php echo($row['nayes']); ?>|<?php echo($row['abstains']); ?></i><br/>
-                                       <?php
-                                               if ($row['status'] ==0 || $_REQUEST['showvotes']) {
-                                                       $state = array('Naye','Abstain','Aye');
-                                                       $vstmt = $db->getStatement("list votes");
-                                                       $vstmt->execute(array($row['id']));
-                                                       echo "<i>Votes:</i><br/>";
-                                                       while ($vrow = $vstmt->fetch()) {
-                                                               echo "<i>".$vrow['name'].": ".$state[$vrow['vote']+1]."</i><br/>";
-                                                       }
-                                               } else {
-                                                       echo '<i><a href="motions.php?motion='.$row['tag'].'&showvotes=1">Show Votes</a></i><br/>';
-                                               }
-                                       ?>
-                                               </td>
-                                       <td class="actions">
-                                               <?php
-                                                       if ($row['status'] == 0 && $user ) {
-                                                               ?>
-                                                               <ul>
-                                                                       <li><a href="vote.php?motion=<?php echo($row['id']); ?>&amp;vote=1">Aye</a></li>
-                                                                       <li><a href="vote.php?motion=<?php echo($row['id']); ?>&amp;vote=0">Abstain</a></li>
-                                                                       <li><a href="vote.php?motion=<?php echo($row['id']); ?>&amp;vote=-1">Naye</a></li>
-                                                                       <li><a href="proxy.php?motion=<?php echo($row['id']); ?>">Proxy Vote</a></li>
-                                                                       <li><a href="motion.php?motion=<?php echo($row['id']); ?>">Modify</a></li>
-                                                                       <li><a href="motions.php?motion=<?php echo($row['tag']); ?>&amp;withdrawl=1">Withdrawl</a></li>
-                                                               </ul>
-                                                               <?php
-                                                       } else {
-                                                               ?>
-                                                               &nbsp;
-                                                               <?php
-                                                       }
-                                               ?>
-                                       </td>
-                                       </tr><?php
-                               }
-                       ?>
-                       <tr>
-                               <td colspan="2" class="navigation">
-                                       <?php if ($page>1) { ?><a href="?page=<?php echo($page-1); ?>">&lt;</a><?php } else { ?>&nbsp;<?php } ?>
-                                               &nbsp;
-                                       <?php if ($items>9) { ?><a href="?page=<?php echo($page+1); ?>">&gt;</a><?php } else { ?>&nbsp;<?php } ?>
-                               </td>
-                               <td class="actions">
-                                       <?php if ($user) echo('<ul><li><a href="motion.php">New Motion</a></li></ul>'); ?>
-                               </td>
-                       </tr>
-                       <?php
-                       if ($_REQUEST['withdrawl']) {
-                       ?>
-                       <tr>
-                               <td colspan="3">
-                       <?php
-                               if ($_REQUEST['confirm'] && $_REQUEST['id']) {
-                       ?>
-                                       <a href="motions.php">Motion Withdrawn</a>
-                       <?php
-                               } else {
-                       ?>
-                                       <form action="?withdrawl=1&amp;confirm=1&amp;id=<?php echo $id;?>" method="post">
-                                               <input type="submit"  value="Withdraw">
-                                       </form>
-                       <?php
-                               }
-                       ?>
-                               </td>
-                       </tr>
-                       <?php
-                       }
-                       ?>
-               </table>
-       </body>
-</html>
diff --git a/notifications.go b/notifications.go
new file mode 100644 (file)
index 0000000..beb9b3f
--- /dev/null
@@ -0,0 +1,171 @@
+package main
+
+import (
+       "bytes"
+       "fmt"
+       "github.com/Masterminds/sprig"
+       "gopkg.in/gomail.v2"
+       "text/template"
+)
+
+type NotificationMail interface {
+       GetData() interface{}
+       GetTemplate() string
+       GetSubject() string
+       GetHeaders() map[string]string
+}
+
+var notifyMail = make(chan NotificationMail, 1)
+var quitMailNotifier = make(chan int)
+
+func CloseMailNotifier() {
+       quitMailNotifier <- 1
+}
+
+func MailNotifier() {
+       logger.Println("Launched mail notifier")
+       for {
+               select {
+               case notification := <-notifyMail:
+                       mailText, err := buildMail(notification.GetTemplate(), notification.GetData())
+                       if err != nil {
+                               logger.Println("ERROR building mail:", err)
+                               continue
+                       }
+
+                       m := gomail.NewMessage()
+                       m.SetHeader("From", config.NoticeSenderAddress)
+                       m.SetHeader("To", config.BoardMailAddress)
+                       m.SetHeader("Subject", notification.GetSubject())
+                       for header, value := range notification.GetHeaders() {
+                               m.SetHeader(header, value)
+                       }
+                       m.SetBody("text/plain", mailText.String())
+
+                       d := gomail.NewDialer(config.MailServer.Host, config.MailServer.Port, "", "")
+                       if err := d.DialAndSend(m); err != nil {
+                               logger.Println("ERROR sending mail:", err)
+                       }
+               case <-quitMailNotifier:
+                       fmt.Println("Ending mail notifier")
+                       return
+               }
+       }
+}
+
+func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer, err error) {
+       t, err := template.New(templateName).Funcs(
+               sprig.GenericFuncMap()).ParseFiles(fmt.Sprintf("templates/%s", templateName))
+       if err != nil {
+               return
+       }
+
+       mailText = bytes.NewBufferString("")
+       t.Execute(mailText, context)
+
+       return
+}
+
+type NotificationClosedDecision struct {
+       decision Decision
+       voteSums VoteSums
+}
+
+func (n *NotificationClosedDecision) GetData() interface{} {
+       return struct {
+               *Decision
+               *VoteSums
+       }{&n.decision, &n.voteSums}
+}
+
+func (n *NotificationClosedDecision) GetTemplate() string {
+       return "closed_motion_mail.txt"
+}
+
+func (n *NotificationClosedDecision) GetSubject() string {
+       return fmt.Sprintf("Re: %s - %s - finalised", n.decision.Tag, n.decision.Title)
+}
+
+func (n *NotificationClosedDecision) GetHeaders() map[string]string {
+       return map[string]string{"References": fmt.Sprintf("<%s>", n.decision.Tag)}
+}
+
+type NotificationCreateMotion struct {
+       decision Decision
+       voter    Voter
+}
+
+func (n *NotificationCreateMotion) GetData() interface{} {
+       voteURL := fmt.Sprintf("%s/vote", config.BaseURL)
+       unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
+       return struct {
+               *Decision
+               Name       string
+               VoteURL    string
+               UnvotedURL string
+       }{&n.decision, n.voter.Name, voteURL, unvotedURL}
+}
+
+func (n *NotificationCreateMotion) GetTemplate() string {
+       return "create_motion_mail.txt"
+}
+
+func (n *NotificationCreateMotion) GetSubject() string {
+       return fmt.Sprintf("%s - %s", n.decision.Tag, n.decision.Title)
+}
+
+func (n *NotificationCreateMotion) GetHeaders() map[string]string {
+       return map[string]string{"Message-ID": fmt.Sprintf("<%s>", n.decision.Tag)}
+}
+
+type NotificationUpdateMotion struct {
+       decision Decision
+       voter    Voter
+}
+
+func (n *NotificationUpdateMotion) GetData() interface{} {
+       voteURL := fmt.Sprintf("%s/vote", config.BaseURL)
+       unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
+       return struct {
+               *Decision
+               Name       string
+               VoteURL    string
+               UnvotedURL string
+       }{&n.decision, n.voter.Name, voteURL, unvotedURL}
+}
+
+func (n *NotificationUpdateMotion) GetTemplate() string {
+       return "update_motion_mail.txt"
+}
+
+func (n *NotificationUpdateMotion) GetSubject() string {
+       return fmt.Sprintf("Re: %s - %s", n.decision.Tag, n.decision.Title)
+}
+
+func (n *NotificationUpdateMotion) GetHeaders() map[string]string {
+       return map[string]string{"References": fmt.Sprintf("<%s>", n.decision.Tag)}
+}
+
+type NotificationWithDrawMotion struct {
+       decision Decision
+       voter    Voter
+}
+
+func (n *NotificationWithDrawMotion) GetData() interface{} {
+       return struct {
+               *Decision
+               Name string
+       }{&n.decision, n.voter.Name}
+}
+
+func (n *NotificationWithDrawMotion) GetTemplate() string {
+       return "withdraw_motion_mail.txt"
+}
+
+func (n *NotificationWithDrawMotion) GetSubject() string {
+       return fmt.Sprintf("Re: %s - %s - withdrawn", n.decision.Tag, n.decision.Title)
+}
+
+func (n *NotificationWithDrawMotion) GetHeaders() map[string]string {
+       return map[string]string{"References": fmt.Sprintf("<%s>", n.decision.Tag)}
+}