| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "log" |
| 7 | "strings" |
| 8 | "time" |
| 9 | ) |
| 10 | |
| 11 | // deliveryChannel names how deliver got a message out. |
| 12 | type deliveryChannel string |
| 13 | |
| 14 | const ( |
| 15 | deliveredKeymail deliveryChannel = "keymail" |
| 16 | deliveredEmail deliveryChannel = "email" |
| 17 | ) |
| 18 | |
| 19 | // domainOf is the lowercased part of addr after its last "@", "" if none. |
| 20 | func domainOf(addr string) string { |
| 21 | i := strings.LastIndex(addr, "@") |
| 22 | if i < 0 { |
| 23 | return "" |
| 24 | } |
| 25 | return strings.ToLower(addr[i+1:]) |
| 26 | } |
| 27 | |
| 28 | // deliver sends one message to an address the person did not just type (an |
| 29 | // invite, a notice): Keymail federation first, then the console mailer (SES). |
| 30 | // The fall-through rule (spec 4.3): |
| 31 | // |
| 32 | // - a definite "no mailbox" (ErrNoKeymailMailbox) goes to the mailer; |
| 33 | // - an unreachable lookup stops when the address is at the Keymail host |
| 34 | // (it IS a Keymail mailbox, so SES must not be used), and otherwise goes |
| 35 | // to the mailer; |
| 36 | // - a block, a price above the cap, or any failure after a successful |
| 37 | // lookup stops with that error: the address is a Keymail mailbox. |
| 38 | // |
| 39 | // keymailShare bounds Keymail's part of ctx so the mailer keeps the rest. |
| 40 | // On error the channel is "". |
| 41 | func (s *Server) deliver(ctx context.Context, to, subject, body string, keymailShare time.Duration) (deliveryChannel, error) { |
| 42 | if s.sendFederated != nil { |
| 43 | kctx, cancel := context.WithTimeout(ctx, keymailShare) |
| 44 | _, err := s.sendFederated(kctx, to, subject, body) |
| 45 | cancel() |
| 46 | if err == nil { |
| 47 | return deliveredKeymail, nil |
| 48 | } |
| 49 | switch { |
| 50 | case IsKeymailBlocked(err): |
| 51 | // The recipient blocked the console. That is their answer; |
| 52 | // email to the same address would go round it. |
| 53 | log.Printf("console: delivery to %s: the recipient blocked the console on keymail; not emailing", to) |
| 54 | return "", err |
| 55 | case errors.Is(err, ErrNoKeymailMailbox): |
| 56 | log.Printf("console: delivery to %s: no keymail mailbox, trying email", to) |
| 57 | case errors.Is(err, ErrKeymailUnreachable): |
| 58 | if s.km != nil && domainOf(to) == s.km.Host() { |
| 59 | log.Printf("console: delivery to %s: keymail lookup unreachable for a keymail address; not emailing: %v", to, err) |
| 60 | return "", err |
| 61 | } |
| 62 | log.Printf("console: delivery to %s: keymail lookup unreachable, trying email: %v", to, err) |
| 63 | default: |
| 64 | // ErrFederationPriceTooHigh, or a failure after the lookup |
| 65 | // said the mailbox exists. |
| 66 | log.Printf("console: delivery to %s by keymail: %v", to, err) |
| 67 | return "", err |
| 68 | } |
| 69 | } |
| 70 | if s.consoleMail == nil { |
| 71 | return "", errors.New("console: no email sender is configured") |
| 72 | } |
| 73 | if err := s.consoleMail.Send(ctx, to, subject, body); err != nil { |
| 74 | log.Printf("console: delivery to %s by email: %v", to, err) |
| 75 | return "", err |
| 76 | } |
| 77 | return deliveredEmail, nil |
| 78 | } |
| 79 | |