Merge pull request #530 from letsencrypt/mailer

Bulk expiration mailer
This commit is contained in:
Jacob Hoffman-Andrews 2015-07-29 17:08:15 -07:00
commit 4d8db36b2e
11 changed files with 591 additions and 15 deletions

View File

@ -17,6 +17,7 @@ OBJECTS = activity-monitor \
boulder-sa \
boulder-va \
boulder-wfe \
expiration-mailer \
ocsp-updater \
ocsp-responder

View File

@ -0,0 +1,280 @@
// Copyright 2015 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"bytes"
"crypto/x509"
"fmt"
"io/ioutil"
"sort"
"strings"
"text/template"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/codegangsta/cli"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/gopkg.in/gorp.v1"
"github.com/letsencrypt/boulder/cmd"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/mail"
"github.com/letsencrypt/boulder/sa"
)
type emailContent struct {
ExpirationDate time.Time
DaysToExpiration int
DNSNames string
}
type mailer struct {
stats statsd.Statter
log *blog.AuditLogger
dbMap *gorp.DbMap
mailer mail.Mailer
emailTemplate *template.Template
nagTimes []time.Duration
limit int
}
func (m *mailer) sendNags(parsedCert *x509.Certificate, contacts []core.AcmeURL) error {
expiresIn := int(parsedCert.NotAfter.Sub(time.Now()).Hours()/24) + 1
emails := []string{}
for _, contact := range contacts {
if contact.Scheme == "mailto" {
emails = append(emails, contact.Opaque)
}
}
if len(emails) > 0 {
email := emailContent{
ExpirationDate: parsedCert.NotAfter,
DaysToExpiration: expiresIn,
DNSNames: strings.Join(parsedCert.DNSNames, ", "),
}
msgBuf := new(bytes.Buffer)
err := m.emailTemplate.Execute(msgBuf, email)
if err != nil {
m.stats.Inc("Mailer.Expiration.Errors.SendingNag.TemplateFailure", 1, 1.0)
return err
}
startSending := time.Now()
err = m.mailer.SendMail(emails, msgBuf.String())
if err != nil {
m.stats.Inc("Mailer.Expiration.Errors.SendingNag.SendFailure", 1, 1.0)
return err
}
m.stats.TimingDuration("Mailer.Expiration.Sending", time.Since(startSending), 1.0)
m.stats.Inc("Mailer.Expiration.Sent", int64(len(emails)), 1.0)
}
return nil
}
func (m *mailer) updateCertStatus(serial string) error {
// Update CertificateStatus object
tx, err := m.dbMap.Begin()
if err != nil {
m.log.Err(fmt.Sprintf("Error opening transaction for certificate %s: %s", serial, err))
tx.Rollback()
return err
}
csObj, err := tx.Get(&core.CertificateStatus{}, serial)
if err != nil {
m.log.Err(fmt.Sprintf("Error fetching status for certificate %s: %s", serial, err))
tx.Rollback()
return err
}
certStatus := csObj.(*core.CertificateStatus)
certStatus.LastExpirationNagSent = time.Now()
_, err = tx.Update(certStatus)
if err != nil {
m.log.Err(fmt.Sprintf("Error updating status for certificate %s: %s", serial, err))
tx.Rollback()
return err
}
err = tx.Commit()
if err != nil {
m.log.Err(fmt.Sprintf("Error commiting transaction for certificate %s: %s", serial, err))
tx.Rollback()
return err
}
return nil
}
func (m *mailer) processCerts(certs []core.Certificate) {
m.log.Info(fmt.Sprintf("expiration-mailer: Found %d certificates, starting sending messages", len(certs)))
for _, cert := range certs {
regObj, err := m.dbMap.Get(&core.Registration{}, cert.RegistrationID)
if err != nil {
m.log.Err(fmt.Sprintf("Error fetching registration %d: %s", cert.RegistrationID, err))
m.stats.Inc("Mailer.Expiration.Errors.GetRegistration", 1, 1.0)
continue
}
reg := regObj.(*core.Registration)
parsedCert, err := x509.ParseCertificate(cert.DER)
if err != nil {
m.log.Err(fmt.Sprintf("Error parsing certificate %s: %s", cert.Serial, err))
m.stats.Inc("Mailer.Expiration.Errors.ParseCertificate", 1, 1.0)
continue
}
err = m.sendNags(parsedCert, reg.Contact)
if err != nil {
m.log.Err(fmt.Sprintf("Error sending nag emails: %s", err))
m.stats.Inc("Mailer.Expiration.Errors.SendingNags", 1, 1.0)
continue
}
err = m.updateCertStatus(cert.Serial)
if err != nil {
m.log.Err(fmt.Sprintf("Error updating certificate status for %s: %s", cert.Serial, err))
m.stats.Inc("Mailer.Expiration.Errors.UpdateCertificateStatus", 1, 1.0)
continue
}
}
m.log.Info("expiration-mailer: Finished sending messages")
return
}
func (m *mailer) findExpiringCertificates() error {
now := time.Now()
// E.g. m.NagTimes = [1, 3, 7, 14] days from expiration
for i, expiresIn := range m.nagTimes {
left := now
if i > 0 {
left = left.Add(m.nagTimes[i-1])
}
right := now.Add(expiresIn)
m.log.Info(fmt.Sprintf("expiration-mailer: Searching for certificates that expire between %s and %s", left, right))
var certs []core.Certificate
_, err := m.dbMap.Select(
&certs,
`SELECT cert.* FROM certificates AS cert
JOIN certificateStatus AS cs
ON cs.serial = cert.serial
JOIN registrations AS reg
ON cert.registrationId = reg.id
WHERE reg.contact LIKE "%mailto%"
AND cert.expires > :cutoffA
AND cert.expires < :cutoffB
AND cert.status != "revoked"
AND cs.lastExpirationNagSent <= :nagCutoff
ORDER BY cert.expires ASC
LIMIT :limit`,
map[string]interface{}{
"cutoffA": left,
"cutoffB": right,
"nagCutoff": time.Now().Add(-expiresIn),
"limit": m.limit,
},
)
if err != nil {
m.log.Err(fmt.Sprintf("expiration-mailer: Error loading certificates: %s", err))
return err // fatal
}
if len(certs) > 0 {
processingStarted := time.Now()
m.processCerts(certs)
m.stats.TimingDuration("Mailer.Expiration.ProcessingCertificates", time.Since(processingStarted), 1.0)
}
}
return nil
}
type durationSlice []time.Duration
func (ds durationSlice) Len() int {
return len(ds)
}
func (ds durationSlice) Less(a, b int) bool {
return ds[a] < ds[b]
}
func (ds durationSlice) Swap(a, b int) {
ds[a], ds[b] = ds[b], ds[a]
}
func main() {
app := cmd.NewAppShell("expiration-mailer")
app.App.Flags = append(app.App.Flags, cli.IntFlag{
Name: "cert_limit",
Value: 100,
EnvVar: "CERT_LIMIT",
Usage: "Count of certificates to process per expiration period",
})
app.Config = func(c *cli.Context, config cmd.Config) cmd.Config {
if c.GlobalInt("cert_limit") > 0 {
config.Mailer.CertLimit = c.GlobalInt("cert_limit")
}
return config
}
app.Action = func(c cmd.Config) {
// Set up logging
stats, err := statsd.NewClient(c.Statsd.Server, c.Statsd.Prefix)
cmd.FailOnError(err, "Couldn't connect to statsd")
auditlogger, err := blog.Dial(c.Syslog.Network, c.Syslog.Server, c.Syslog.Tag, stats)
cmd.FailOnError(err, "Could not connect to Syslog")
// AUDIT[ Error Conditions ] 9cc4d537-8534-4970-8665-4b382abe82f3
defer auditlogger.AuditPanic()
blog.SetAuditLogger(auditlogger)
auditlogger.Info(app.VersionString())
go cmd.DebugServer(c.Mailer.DebugAddr)
// Configure DB
dbMap, err := sa.NewDbMap(c.Mailer.DBDriver, c.Mailer.DBConnect)
cmd.FailOnError(err, "Could not connect to database")
// Load email template
emailTmpl, err := ioutil.ReadFile(c.Mailer.EmailTemplate)
cmd.FailOnError(err, fmt.Sprintf("Could not read email template file [%s]", c.Mailer.EmailTemplate))
tmpl, err := template.New("expiry-email").Parse(string(emailTmpl))
cmd.FailOnError(err, "Could not parse email template")
mailClient := mail.New(c.Mailer.Server, c.Mailer.Port, c.Mailer.Username, c.Mailer.Password)
var nags durationSlice
for _, nagDuration := range c.Mailer.NagTimes {
dur, err := time.ParseDuration(nagDuration)
if err != nil {
auditlogger.Err(fmt.Sprintf("Failed to parse nag duration string [%s]: %s", nagDuration, err))
return
}
nags = append(nags, dur)
}
// Make sure durations are sorted in increasing order
sort.Sort(nags)
m := mailer{
stats: stats,
log: auditlogger,
dbMap: dbMap,
mailer: &mailClient,
emailTemplate: tmpl,
nagTimes: nags,
limit: c.Mailer.CertLimit,
}
auditlogger.Info("expiration-mailer: Starting")
err = m.findExpiringCertificates()
cmd.FailOnError(err, "expiration-mailer has failed")
}
app.Run()
}

View File

@ -0,0 +1,257 @@
// Copyright 2015 ISRG. All rights reserved
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"fmt"
"math/big"
"net/url"
"testing"
"text/template"
"time"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/letsencrypt/go-jose"
"github.com/letsencrypt/boulder/core"
blog "github.com/letsencrypt/boulder/log"
"github.com/letsencrypt/boulder/mocks"
"github.com/letsencrypt/boulder/sa"
"github.com/letsencrypt/boulder/test"
)
func bigIntFromB64(b64 string) *big.Int {
bytes, _ := base64.URLEncoding.DecodeString(b64)
x := big.NewInt(0)
x.SetBytes(bytes)
return x
}
func intFromB64(b64 string) int {
return int(bigIntFromB64(b64).Int64())
}
type mockMail struct {
Messages []string
}
func (m *mockMail) Clear() {
m.Messages = []string{}
}
func (m *mockMail) SendMail(to []string, msg string) (err error) {
for _ = range to {
m.Messages = append(m.Messages, msg)
}
return
}
const testTmpl = `hi, cert for DNS names {{.DNSNames}} is going to expire in {{.DaysToExpiration}} days ({{.ExpirationDate}})`
var jsonKeyA = []byte(`{
"kty":"RSA",
"n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB"
}`)
var jsonKeyB = []byte(`{
"kty":"RSA",
"n":"z8bp-jPtHt4lKBqepeKF28g_QAEOuEsCIou6sZ9ndsQsEjxEOQxQ0xNOQezsKa63eogw8YS3vzjUcPP5BJuVzfPfGd5NVUdT-vSSwxk3wvk_jtNqhrpcoG0elRPQfMVsQWmxCAXCVRz3xbcFI8GTe-syynG3l-g1IzYIIZVNI6jdljCZML1HOMTTW4f7uJJ8mM-08oQCeHbr5ejK7O2yMSSYxW03zY-Tj1iVEebROeMv6IEEJNFSS4yM-hLpNAqVuQxFGetwtwjDMC1Drs1dTWrPuUAAjKGrP151z1_dE74M5evpAhZUmpKv1hY-x85DC6N0hFPgowsanmTNNiV75w",
"e":"AAEAAQ"
}`)
var log = mocks.UseMockLog()
func TestSendNags(t *testing.T) {
tmpl, err := template.New("expiry-email").Parse(testTmpl)
test.AssertNotError(t, err, "Couldn't parse test email template")
stats, _ := statsd.NewNoopClient(nil)
mc := mockMail{}
m := mailer{
stats: stats,
mailer: &mc,
emailTemplate: tmpl,
}
cert := &x509.Certificate{
Subject: pkix.Name{
CommonName: "happy",
},
NotAfter: time.Now().AddDate(0, 0, 2),
DNSNames: []string{"example.com"},
}
email, _ := url.Parse("mailto:rolandshoemaker@gmail.com")
emailB, _ := url.Parse("mailto:test@gmail.com")
err = m.sendNags(cert, []core.AcmeURL{core.AcmeURL(*email)})
test.AssertNotError(t, err, "Failed to send warning messages")
test.AssertEquals(t, len(mc.Messages), 1)
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter), mc.Messages[0])
mc.Clear()
err = m.sendNags(cert, []core.AcmeURL{core.AcmeURL(*email), core.AcmeURL(*emailB)})
test.AssertNotError(t, err, "Failed to send warning messages")
test.AssertEquals(t, len(mc.Messages), 2)
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter), mc.Messages[0])
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example.com is going to expire in 2 days (%s)`, cert.NotAfter), mc.Messages[1])
mc.Clear()
err = m.sendNags(cert, []core.AcmeURL{})
test.AssertNotError(t, err, "Not an error to pass no email contacts")
test.AssertEquals(t, len(mc.Messages), 0)
}
var n = bigIntFromB64("n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw==")
var e = intFromB64("AQAB")
var d = bigIntFromB64("bWUC9B-EFRIo8kpGfh0ZuyGPvMNKvYWNtB_ikiH9k20eT-O1q_I78eiZkpXxXQ0UTEs2LsNRS-8uJbvQ-A1irkwMSMkK1J3XTGgdrhCku9gRldY7sNA_AKZGh-Q661_42rINLRCe8W-nZ34ui_qOfkLnK9QWDDqpaIsA-bMwWWSDFu2MUBYwkHTMEzLYGqOe04noqeq1hExBTHBOBdkMXiuFhUq1BU6l-DqEiWxqg82sXt2h-LMnT3046AOYJoRioz75tSUQfGCshWTBnP5uDjd18kKhyv07lhfSJdrPdM5Plyl21hsFf4L_mHCuoFau7gdsPfHPxxjVOcOpBrQzwQ==")
var p = bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=")
var q = bigIntFromB64("uKE2dh-cTf6ERF4k4e_jy78GfPYUIaUyoSSJuBzp3Cubk3OCqs6grT8bR_cu0Dm1MZwWmtdqDyI95HrUeq3MP15vMMON8lHTeZu2lmKvwqW7anV5UzhM1iZ7z4yMkuUwFWoBvyY898EXvRD-hdqRxHlSqAZ192zB3pVFJ0s7pFc=")
var testKey = rsa.PrivateKey{
PublicKey: rsa.PublicKey{N: n, E: e},
D: d,
Primes: []*big.Int{p, q},
}
func TestFindExpiringCertificates(t *testing.T) {
dbMap, err := sa.NewDbMap("sqlite3", ":memory:")
test.AssertNotError(t, err, "Couldn't connect to SQLite")
err = dbMap.CreateTablesIfNotExists()
test.AssertNotError(t, err, "Couldn't create tables")
tmpl, err := template.New("expiry-email").Parse(testTmpl)
test.AssertNotError(t, err, "Couldn't parse test email template")
stats, _ := statsd.NewNoopClient(nil)
mc := mockMail{}
m := mailer{
log: blog.GetAuditLogger(),
stats: stats,
mailer: &mc,
emailTemplate: tmpl,
dbMap: dbMap,
nagTimes: []time.Duration{time.Hour * 24, time.Hour * 24 * 4, time.Hour * 24 * 7},
limit: 100,
}
log.Clear()
err = m.findExpiringCertificates()
test.AssertNotError(t, err, "Failed on no certificates")
test.AssertEquals(t, len(log.GetAllMatching("Searching for certificates that expire between.*")), 3)
// Add some expiring certificates and registrations
emailA, _ := url.Parse("mailto:one@mail.com")
emailB, _ := url.Parse("mailto:twp@mail.com")
var keyA jose.JsonWebKey
var keyB jose.JsonWebKey
err = json.Unmarshal(jsonKeyA, &keyA)
test.AssertNotError(t, err, "Failed to unmarshal public JWK")
err = json.Unmarshal(jsonKeyB, &keyB)
test.AssertNotError(t, err, "Failed to unmarshal public JWK")
regA := &core.Registration{
ID: 1,
Contact: []core.AcmeURL{
core.AcmeURL(*emailA),
},
Key: keyA,
}
regB := &core.Registration{
ID: 2,
Contact: []core.AcmeURL{
core.AcmeURL(*emailB),
},
Key: keyB,
}
rawCertA := x509.Certificate{
Subject: pkix.Name{
CommonName: "happy A",
},
NotAfter: time.Now().AddDate(0, 0, 1),
DNSNames: []string{"example-a.com"},
SerialNumber: big.NewInt(1337),
}
certDerA, _ := x509.CreateCertificate(rand.Reader, &rawCertA, &rawCertA, &testKey.PublicKey, &testKey)
certA := &core.Certificate{
RegistrationID: 1,
Status: core.StatusValid,
Serial: "001",
Expires: time.Now().AddDate(0, 0, 1),
DER: certDerA,
}
// Already sent a nag but too long ago
certStatusA := &core.CertificateStatus{Serial: "001", LastExpirationNagSent: time.Now().Add(-time.Hour * 24 * 3)}
rawCertB := x509.Certificate{
Subject: pkix.Name{
CommonName: "happy B",
},
NotAfter: time.Now().AddDate(0, 0, 3),
DNSNames: []string{"example-b.com"},
SerialNumber: big.NewInt(1337),
}
certDerB, _ := x509.CreateCertificate(rand.Reader, &rawCertB, &rawCertB, &testKey.PublicKey, &testKey)
certB := &core.Certificate{
RegistrationID: 1,
Status: core.StatusValid,
Serial: "002",
Expires: time.Now().AddDate(0, 0, 3),
DER: certDerB,
}
// Already sent a nag for this period
certStatusB := &core.CertificateStatus{Serial: "002", LastExpirationNagSent: time.Now().Add(-time.Hour * 24 * 3)}
rawCertC := x509.Certificate{
Subject: pkix.Name{
CommonName: "happy C",
},
NotAfter: time.Now().AddDate(0, 0, 7),
DNSNames: []string{"example-c.com"},
SerialNumber: big.NewInt(1337),
}
certDerC, _ := x509.CreateCertificate(rand.Reader, &rawCertC, &rawCertC, &testKey.PublicKey, &testKey)
certC := &core.Certificate{
RegistrationID: 2,
Status: core.StatusValid,
Serial: "003",
Expires: time.Now().AddDate(0, 0, 7),
DER: certDerC,
}
certStatusC := &core.CertificateStatus{Serial: "003"}
err = dbMap.Insert(regA)
test.AssertNotError(t, err, "Couldn't add regA")
err = dbMap.Insert(regB)
test.AssertNotError(t, err, "Couldn't add regB")
err = dbMap.Insert(certA)
test.AssertNotError(t, err, "Couldn't add certA")
err = dbMap.Insert(certB)
test.AssertNotError(t, err, "Couldn't add certB")
err = dbMap.Insert(certC)
test.AssertNotError(t, err, "Couldn't add certC")
err = dbMap.Insert(certStatusA)
test.AssertNotError(t, err, "Couldn't add certStatusA")
err = dbMap.Insert(certStatusB)
test.AssertNotError(t, err, "Couldn't add certStatusB")
err = dbMap.Insert(certStatusC)
test.AssertNotError(t, err, "Couldn't add certStatusC")
log.Clear()
err = m.findExpiringCertificates()
test.AssertNotError(t, err, "Failed to find expiring certs")
// Should get 001 and 003
test.AssertEquals(t, len(mc.Messages), 2)
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example-a.com is going to expire in 1 days (%s)`, rawCertA.NotAfter.UTC().Format("2006-01-02 15:04:05 -0700 MST")), mc.Messages[0])
test.AssertEquals(t, fmt.Sprintf(`hi, cert for DNS names example-c.com is going to expire in 7 days (%s)`, rawCertC.NotAfter.UTC().Format("2006-01-02 15:04:05 -0700 MST")), mc.Messages[1])
// A consecutive run shouldn't find anything
mc.Clear()
log.Clear()
err = m.findExpiringCertificates()
test.AssertNotError(t, err, "Failed to find expiring certs")
test.AssertEquals(t, len(mc.Messages), 0)
}

View File

@ -98,7 +98,7 @@ func (src *DBSource) Response(req *ocsp.Request) (response []byte, present bool)
log.Debug(fmt.Sprintf("Searching for OCSP issued by us for serial %s", serialString))
var ocspResponse core.OCSPResponse
err := src.dbMap.SelectOne(&ocspResponse, "SELECT * from ocspResponses WHERE serial = :serial ORDER BY createdAt DESC LIMIT 1;",
err := src.dbMap.SelectOne(&ocspResponse, "SELECT * from ocspResponses WHERE serial = :serial ORDER BY createdAt DESC LIMIT 1;",
map[string]interface{}{"serial": serialString})
if err != nil {
present = false

View File

@ -130,11 +130,22 @@ type Config struct {
DBConnect string
}
Mail struct {
Mailer struct {
Server string
Port string
Username string
Password string
DBDriver string
DBConnect string
CertLimit int
NagTimes []string
// Path to a text/template email template
EmailTemplate string
// DebugAddr is the address to run the /debug handlers on.
DebugAddr string
}
OCSPResponder struct {
@ -273,7 +284,7 @@ func AmqpChannel(conf Config) (*amqp.Channel, error) {
if conf.AMQP.TLS.CertFile != nil || conf.AMQP.TLS.KeyFile != nil {
// But they have to give both.
if conf.AMQP.TLS.CertFile == nil || conf.AMQP.TLS.KeyFile == nil {
err = fmt.Errorf("AMQPS: You must set both of the configuration values AMQP.TLS.KeyFile and AMQP.TLS.CertFile.")
err = fmt.Errorf("AMQPS: You must set both of the configuration values AMQP.TLS.KeyFile and AMQP.TLS.CertFile")
return nil, err
}

View File

@ -562,6 +562,8 @@ type CertificateStatus struct {
// code for 'unspecified').
RevokedReason int `db:"revokedReason"`
LastExpirationNagSent time.Time `db:"lastExpirationNagSent"`
LockCol int64 `json:"-"`
}

View File

@ -6,22 +6,28 @@
package mail
import (
"net"
"net/smtp"
)
// Mailer defines a mail transfer agent to use for sending mail
type Mailer struct {
// Mailer provides the interface for a mailer
type Mailer interface {
SendMail([]string, string) error
}
// MailerImpl defines a mail transfer agent to use for sending mail
type MailerImpl struct {
Server string
Port string
Auth smtp.Auth
From string
}
// NewMailer constructs a Mailer to represent an account at a particular mail
// New constructs a Mailer to represent an account on a particular mail
// transfer agent.
func NewMailer(server, port, username, password string) Mailer {
func New(server, port, username, password string) MailerImpl {
auth := smtp.PlainAuth("", username, password, server)
return Mailer{
return MailerImpl{
Server: server,
Port: port,
Auth: auth,
@ -31,7 +37,7 @@ func NewMailer(server, port, username, password string) Mailer {
// SendMail sends an email to the provided list of recipients. The email body
// is simple text.
func (m *Mailer) SendMail(to []string, msg string) (err error) {
err = smtp.SendMail(m.Server+":"+m.Port, m.Auth, m.From, to, []byte(msg))
func (m *MailerImpl) SendMail(to []string, msg string) (err error) {
err = smtp.SendMail(net.JoinHostPort(m.Server, m.Port), m.Auth, m.From, to, []byte(msg))
return
}

View File

@ -17,7 +17,8 @@ TESTDIRS="analysis \
sa \
test \
va \
wfe"
wfe \
cmd/expiration-mailer"
# cmd
# Godeps

View File

@ -157,11 +157,17 @@
"debugAddr": "localhost:8007"
},
"mail": {
"mailer": {
"server": "mail.example.com",
"port": "25",
"username": "cert-master@example.com",
"password": "password"
"password": "password",
"dbDriver": "sqlite3",
"dbConnect": ":memory:",
"messageLimit": 0,
"nagTimes": ["24h", "72h", "168h", "336h"],
"emailTemplate": "test/example-expiration-template",
"debugAddr": "localhost:8004"
},
"common": {

View File

@ -144,11 +144,17 @@
"debugAddr": "localhost:8006"
},
"mail": {
"mailer": {
"server": "mail.example.com",
"port": "25",
"username": "cert-master@example.com",
"password": "password"
"password": "password",
"dbDriver": "sqlite3",
"dbConnect": ":memory:",
"messageLimit": 0,
"nagTimes": ["24h", "72h", "168h", "336h"],
"emailTemplate": "test/example-expiration-template",
"debugAddr": "localhost:8004"
},
"common": {

View File

@ -0,0 +1,6 @@
Hello,
Your SSL certificate for names {{.DNSNames}} is going to expire in {{.DaysToExpiration}}
days ({{.ExpirationDate}}), make sure you run the renewer before then!
Regards