// inbound.go holds what RFC 0014 Phase 1 needs the forge to remember
// about replies that arrive through notify: the secret notify signs
// its deliveries with, and which replies have already been applied.
//
// Both are forge-local state, not truth: a lost secret is rotated
// and stored again, a lost dedup row can at worst let one retried
// delivery post twice.
package oltp
import (
"database/sql"
"time"
)
const inboundSchema = `
CREATE TABLE IF NOT EXISTS notify_hook (
id INTEGER NOT NULL PRIMARY KEY CHECK (id = 1),
secret TEXT NOT NULL,
endpoint_id TEXT NOT NULL DEFAULT '',
url TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS inbound_reply (
message_id TEXT NOT NULL PRIMARY KEY, -- the mail's Message-ID, notify's delivery key
txn_id TEXT NOT NULL,
account TEXT NOT NULL,
outcome TEXT NOT NULL, -- what the reply became, or why it did not
at TEXT NOT NULL
);`
// NotifyHookSecret is the key notify signs reply deliveries with, or
// empty when the forge holds none yet. notify mints it and shows it
// once, at the endpoint's creation or rotation; the forge keeps that
// one copy here. No key means every delivery is refused, which is the
// closed side of the door.
func (d *DB) NotifyHookSecret() (string, error) {
var secret string
err := d.db.QueryRow(`SELECT secret FROM notify_hook WHERE id = 1`).Scan(&secret)
if err == sql.ErrNoRows {
return "", nil
}
return secret, err
}
// SetNotifyHook records the endpoint notify created for the forge and
// the secret it signs with.
func (d *DB) SetNotifyHook(id, url, secret string) error {
_, err := d.db.Exec(`INSERT OR REPLACE INTO notify_hook (id, secret, endpoint_id, url)
VALUES (1, ?, ?, ?)`, secret, id, url)
return err
}
// InboundReplySeen answers whether a delivery was already applied and
// what it became. notify redelivers on any non-2xx and on transport
// error, so the same mail can arrive more than once; one mail is one
// post.
func (d *DB) InboundReplySeen(messageID string) (outcome string, seen bool, err error) {
err = d.db.QueryRow(`SELECT outcome FROM inbound_reply WHERE message_id = ?`, messageID).Scan(&outcome)
if err == sql.ErrNoRows {
return "", false, nil
}
if err != nil {
return "", false, err
}
return outcome, true, nil
}
// RecordInboundReply remembers what a delivery became.
func (d *DB) RecordInboundReply(messageID, txnID, account, outcome string) error {
_, err := d.db.Exec(`INSERT OR REPLACE INTO inbound_reply (message_id, txn_id, account, outcome, at)
VALUES (?, ?, ?, ?, ?)`, messageID, txnID, account, outcome, time.Now().UTC().Format(time.RFC3339))
return err
}