diff options
-rw-r--r-- | actions.go | 131 | ||||
-rw-r--r-- | boardvoting.go | 24 | ||||
-rw-r--r-- | denied.php | 12 | ||||
-rw-r--r-- | index.php | 5 | ||||
-rw-r--r-- | models.go | 85 | ||||
-rw-r--r-- | motion.php | 198 | ||||
-rw-r--r-- | motions.php | 167 | ||||
-rw-r--r-- | notifications.go | 171 |
8 files changed, 276 insertions, 517 deletions
diff --git a/actions.go b/actions.go deleted file mode 100644 index 19e9bf3..0000000 --- a/actions.go +++ /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 -} diff --git a/boardvoting.go b/boardvoting.go index 187a867..30de17e 100644 --- a/boardvoting.go +++ b/boardvoting.go @@ -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 index 9bb72d6..0000000 --- a/denied.php +++ /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 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 @@ -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 index 2dec354..0000000 --- a/motion.php +++ /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> </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 index 548731f..0000000 --- a/motions.php +++ /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']); ?>&vote=1">Aye</a></li> - <li><a href="vote.php?motion=<?php echo($row['id']); ?>&vote=0">Abstain</a></li> - <li><a href="vote.php?motion=<?php echo($row['id']); ?>&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']); ?>&withdrawl=1">Withdrawl</a></li> - </ul> - <?php - } else { - ?> - - <?php - } - ?> - </td> - </tr><?php - } - ?> - <tr> - <td colspan="2" class="navigation"> - <?php if ($page>1) { ?><a href="?page=<?php echo($page-1); ?>"><</a><?php } else { ?> <?php } ?> - - <?php if ($items>9) { ?><a href="?page=<?php echo($page+1); ?>">></a><?php } else { ?> <?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&confirm=1&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 index 0000000..beb9b3f --- /dev/null +++ b/notifications.go @@ -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)} +} |