Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jareklupinski committed Jan 6, 2021
0 parents commit 3547c4d
Show file tree
Hide file tree
Showing 12 changed files with 403 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.DS_Store
.env
.idea/
bin/
vendor/
2 changes: 2 additions & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
web: bin/web
worker: bin/worker
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

# ⏰ 🐕 . 📧

## Watchdog.Email

Full source code for http://watchdog.email

Inspired by a lack of simple watchdog timers, I set out to see what makes them so difficult.

Currently deployed to Heroku with the following Apps:
- Heroku Redis
- Heroku Scheduler
- Papertrail
- SendGrid

2 Free Dynos are used to run the service:
- web - handles frontend http requests
- worker - runs on-schedule to send emails
12 changes: 12 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "Watchdog.Email",
"description": "A Service That Sends You An Email In 25 Hours",
"keywords": [
"watchdog",
"timer",
"email",
"service"
],
"website": "http://watchdog.email",
"repository": "http://github.com/jareklupinski/watchdog-email"
}
16 changes: 16 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module watchdog-email

go 1.12

require (
github.com/gin-gonic/gin v0.0.0-20150626140855-4cc2de6207f4
github.com/gomodule/redigo v1.8.3
github.com/heroku/x v0.0.0-20171004170240-705849e307dd
github.com/manucorporat/sse v0.0.0-20150604091100-c142f0f1baea // indirect
github.com/mattn/go-colorable v0.0.0-20150625154642-40e4aedc8fab // indirect
github.com/mattn/go-isatty v0.0.0-20150814002629-7fcbc72f853b // indirect
github.com/sendgrid/rest v2.6.2+incompatible // indirect
github.com/sendgrid/sendgrid-go v3.7.2+incompatible
golang.org/x/net v0.0.0-20150629084131-d9558e5c97f8 // indirect
gopkg.in/bluesuncorp/validator.v5 v5.9.1 // indirect
)
31 changes: 31 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-gonic/gin v0.0.0-20150626140855-4cc2de6207f4 h1:ufr+93X0/9xTNvObfvbHsEkgCk8BrhmUH83Z8YIhzXE=
github.com/gin-gonic/gin v0.0.0-20150626140855-4cc2de6207f4/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/gomodule/redigo v1.8.3 h1:HR0kYDX2RJZvAup8CsiJwxB4dTCSC0AaUq6S4SiLwUc=
github.com/gomodule/redigo v1.8.3/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
github.com/heroku/x v0.0.0-20171004170240-705849e307dd h1:zn29UrzyUeQgqxBGXIwQqQJf75IiK4aeCtO5q1V2Vyo=
github.com/heroku/x v0.0.0-20171004170240-705849e307dd/go.mod h1:opmAyjmIGn9/Y+9Nia6eIaktIXIoMhhFXEFbHLMsX3Y=
github.com/manucorporat/sse v0.0.0-20150604091100-c142f0f1baea h1:3she1OMibtVtGiZSF65Cfi5ijRb+pAKXmstffNs5i+4=
github.com/manucorporat/sse v0.0.0-20150604091100-c142f0f1baea/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM=
github.com/mattn/go-colorable v0.0.0-20150625154642-40e4aedc8fab h1:3lgod/2wdM8WaJNBe16LkFmrz2XkXbH2YhbRWh38W9U=
github.com/mattn/go-colorable v0.0.0-20150625154642-40e4aedc8fab/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.0-20150814002629-7fcbc72f853b h1:KOTLb2pwNaE8UOCVz1AgsFYzcswaNroRkBDIhblvUFk=
github.com/mattn/go-isatty v0.0.0-20150814002629-7fcbc72f853b/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sendgrid/rest v2.6.2+incompatible h1:zGMNhccsPkIc8SvU9x+qdDz2qhFoGUPGGC4mMvTondA=
github.com/sendgrid/rest v2.6.2+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.7.2+incompatible h1:ePQr9ns8so+28whk+gLKRYiyI5IiCESkDIqy7cjiwLg=
github.com/sendgrid/sendgrid-go v3.7.2+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
golang.org/x/net v0.0.0-20150629084131-d9558e5c97f8 h1:NnqNZS6fS9JZOjzXyL3/g0j4bEm1B7HCkiVH9F5Zu8U=
golang.org/x/net v0.0.0-20150629084131-d9558e5c97f8/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
gopkg.in/bluesuncorp/validator.v5 v5.9.1 h1:XEU2HtMj0Rki3kmHh+uilvENyWgDEaR5LDLtYsjiumM=
gopkg.in/bluesuncorp/validator.v5 v5.9.1/go.mod h1:ScQmud/GM3iSR85jRE+8BI8E8oFv5oj4qyd5Xaw7hgE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
22 changes: 22 additions & 0 deletions static/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.jumbotron {
background: #532F8C;
color: white;
padding-bottom: 80px;
}

.jumbotron .btn {
background: #a49cb1;
color: white;
border-color: #845ac7;
margin-left: 0.75em;
}

.jumbotron .btn:hover {
background: #9580b7;
}

.jumbotron p {
color: #d9ccee;
max-width: 75%;
margin: 1em auto 2em;
}
90 changes: 90 additions & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<html lang="en">
<head>
<title>Watchdog.Email</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script type="text/javascript" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js"></script>
<link rel="stylesheet" type="text/css" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css"/>
<link rel="stylesheet" type="text/css" href="/static/main.css"/>
</head>
<body>
<div class="jumbotron text-center">
<div class="container">
<h1>
⏰ 🐕 . 📧
</h1>
<h2>
Watchdog.Email
</h2>
<p>
A Service That Sends You An Email In 25 Hours
</p>
<form class="form-inline" action="/" method="get">
<div class="form-group form-group-lg">
<label class="sr-only" for="email">Email address</label>
<input type="email" class="form-control form-control-lg" id="email" name="email" placeholder="Email">
</div>
<input type="submit" class="btn btn-lg btn-default" value="Start Watchdog">
</form>
</div>
</div>
<div class="container">
<div class="alert alert-info text-center">
Privacy Policy: We will only ever send you one email; when your Watchdog times out after 25 hours. After the email is sent, your address is wiped from our database.
</div>
<hr>
<div class="row">
<div class="col-md-6">
<h3>
<span class="glyphicon glyphicon-info-sign"></span>
How This Works
</h3>
<ul>
<li>
When you type in an email address and click Start Watchdog, a 25 Hour Watchdog Timer starts running on our servers.
</li>
<li>
After 25 hours have elapsed, Watchdog.Email sends an email to the address you specified notifying that the Watchdog Timer has run out.
</li>
<li>
If you want to delay the email from arriving for another 25 hours, type the same email address into the field again and click Start Watchdog to restart your Watchdog.
</li>
<li>
You can repeat this process as many times as you like; as long as you keep restarting your Watchdog within 25 hours, you will not receive an email.
</li>
<li>
You can also use this service programmatically by sending your Watchdog.Email request to <code>https://watchdog.email/?email=&lt;email_address&gt;</code>.
</li>
</ul>
</div>
<div class="col-md-6">
<h3>
<span class="glyphicon glyphicon-link"></span>
Further Reading
</h3>
<ul>
<li>
Wikipedia: A <a href="https://en.wikipedia.org/wiki/Watchdog_timer">watchdog timer</a> (sometimes called a
computer operating properly or COP timer, or simply a watchdog) is an electronic or software timer that is
used to detect and recover from computer malfunctions. During normal operation, the computer regularly resets
the watchdog timer to prevent it from elapsing, or "timing out". If, due to a hardware fault or program error,
the computer fails to reset the watchdog, the timer will elapse and generate a timeout signal. The timeout
signal is used to initiate corrective actions. The corrective actions typically include placing the computer
system in a safe state and restoring normal system operation.
</li>
<li>
Github: Full Source Code available at
<a href="https://github.com/jareklupinski/watchdog-email">https://github.com/jareklupinski/watchdog-email</a>
</li>
</ul>
</div>
</div>
<hr>
<div class="alert alert-info text-center">
We have no idea how to monetize this, and will continue to keep this running on a Heroku Free Tier until it begins accruing a billing rate above which we can spend.
If there is any danger to the service shutting down, we'll solicit some donations to keep the cost alive via Hackernews and Github.
If this still fails we will shutdown the incoming reset button and link for 25 hours by returning any calls to it with <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402">a 402 error message</a>
to allow the last day's worth of watchdogs to elapse.
</div>
</div>
</body>
</html>
47 changes: 47 additions & 0 deletions util/email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package util

import (
"github.com/sendgrid/sendgrid-go"
"github.com/sendgrid/sendgrid-go/helpers/mail"
"log"
"net"
"os"
"regexp"
"strings"
)

var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")

func EmailIsValid(e string) bool {
if len(e) < 3 && len(e) > 254 {
return false
}
if !emailRegex.MatchString(e) {
return false
}
parts := strings.Split(e, "@")
mx, err := net.LookupMX(parts[1])
if err != nil || len(mx) == 0 {
return false
}
return true
}

func SendEmail(emailAddress string) {
from := mail.NewEmail("⏰🐕.📧", "[email protected]")
subject := "Your Watchdog.Email Timer has Fired!"
to := mail.NewEmail(emailAddress, emailAddress)
plainTextContent := "Reset your timer: http://watchdog.email/" + emailAddress
htmlContent := "Reset your timer: <a href=\"http://watchdog.email/" + emailAddress + "\">http://watchdog.email/" + emailAddress + "</a>"
message := mail.NewSingleEmail(from, subject, to, plainTextContent, htmlContent)
client := sendgrid.NewSendClient(os.Getenv("SENDGRID_API_KEY"))

response, err := client.Send(message)
if err != nil || response.StatusCode != 200 {
if response != nil {
log.Printf("Failed to send email to %s: %s, SendGrid Response %d: %s\n", emailAddress, err, response.StatusCode, response.Body)
} else {
log.Printf("Failed to send email to %s: %s, No SendGrid Response\n", emailAddress, err)
}
}
}
39 changes: 39 additions & 0 deletions util/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package util

import (
"log"
"os"

"github.com/gomodule/redigo/redis"
)

type RedisController struct {
Conn redis.Conn
}

func NewRedisController() *RedisController {
redisURL := os.Getenv("REDIS_URL")
redisConnection, err := redis.DialURL(redisURL)
if err != nil {
log.Panicf("Failed to connect to $REDIS_URL %s\n", redisURL)
}
redisContext := RedisController{
Conn: redisConnection,
}
return &redisContext
}

func (r *RedisController) GetRedisConnection() redis.Conn {
rds := r.Conn
if rds == nil {
log.Panic("Redis Connection attempted before Redis Initialized!")
}
return rds
}

func (r *RedisController) CloseRedisController() {
err := r.Conn.Close()
if err != nil {
log.Panicf("Failed to close connection to redis %s\n", err)
}
}
81 changes: 81 additions & 0 deletions web/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package main

import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"time"

"github.com/gin-gonic/gin"
"github.com/gomodule/redigo/redis"
_ "github.com/heroku/x/hmetrics/onload"

"watchdog-email/util"
)

func startWatchdog(r *util.RedisController) gin.HandlerFunc {
fn := func(c *gin.Context) {
email := c.Query("email")
if email == "" {
c.HTML(http.StatusOK, "index.html", nil)
return
}
if !util.EmailIsValid(email) {
c.String(http.StatusBadRequest, "Cannot set Watchdog.Email timer for %s", email)
return
}
rds := r.GetRedisConnection()
now := time.Now().Unix()
alarm := now + 90000 // (60 seconds / minute) * (60 minutes / hour) * (25 hours / timeout)
rows, err := redis.Int(rds.Do("ZADD", "email", "CH", alarm, email))
if err != nil || rows < 1 {
log.Printf("Failed to set Watchdog.Email timer for %s: %s", email, err)
c.String(http.StatusInternalServerError, "Failed to set Watchdog.Email timer for %s", email)
return
}
c.String(http.StatusOK, "Watchdog.Email has been set at %d to send an email to %s at %d", now, email, alarm)
}
return fn
}

func main() {
log.Println("Watchdog.Email Server Starting")
redisContext := util.NewRedisController()

port := os.Getenv("PORT")
if port == "" {
log.Panic("$PORT must be set")
}

gin.SetMode(gin.ReleaseMode)
router := gin.New()
router.Use(gin.Logger())
router.LoadHTMLGlob("templates/*.html")
router.Static("/static", "static")
router.GET("/", startWatchdog(redisContext))
srv := &http.Server{
Addr: fmt.Sprintf(":%s", port),
Handler: router,
}

go func() {
log.Println("Watchdog.Email Server Running")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Panicf("Server Error: %s", err)
}
}()

quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Panicf("Server forced to shutdown: %s", err)
}
redisContext.CloseRedisController()
log.Println("Watchdog.Email Server Exiting")
}
Loading

0 comments on commit 3547c4d

Please sign in to comment.