-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 3547c4d
Showing
12 changed files
with
403 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
.DS_Store | ||
.env | ||
.idea/ | ||
bin/ | ||
vendor/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
web: bin/web | ||
worker: bin/worker |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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=<email_address></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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
Oops, something went wrong.