noncer/announcements/announcements.go
jolheiser d4707ab8c1
feat: run check before starting ticker
Signed-off-by: jolheiser <john.olheiser@gmail.com>
2022-11-04 12:46:18 -05:00

167 lines
4.0 KiB
Go

package announcements
import (
"context"
"fmt"
"strings"
"time"
"github.com/BrianLeishman/go-imap"
md "github.com/JohannesKaufmann/html-to-markdown"
"github.com/caarlos0/log"
)
const discordMessageSize = 1990
type Announcement struct {
Subject string
Contents []string
}
func Listen(ctx context.Context, im *imap.Dialer, announcements chan<- Announcement, period int, allowedDomains []string) error {
check(im, announcements, allowedDomains)
t := time.NewTicker(time.Duration(period) * time.Second)
for {
select {
case <-ctx.Done():
close(announcements)
return ctx.Err()
case <-t.C:
check(im, announcements, allowedDomains)
}
}
}
func check(im *imap.Dialer, announcements chan<- Announcement, allowedDomains []string) {
err := SelectFolder(im, "INBOX")
if err != nil {
log.WithError(err).Error("could not select inbox")
return
}
// gets all emails in current inbox
emails, err := im.GetEmails()
if err != nil {
log.WithError(err).Error("could not get inbox emails")
return
}
var toRemove []int
for uid, email := range emails {
toRemove = append(toRemove, uid)
if !valid(allowedDomains, email) {
log.Infof("received invalid email from %s", email.From)
continue
}
body, err := markdownBody(email.HTML)
if err != nil {
log.WithError(err).Error("could not create announcement")
continue
}
announcements <- Announcement{
Subject: email.Subject,
Contents: buildContents(discordMessageSize, email.Subject, body),
}
}
// no emails were found, so dont bother removing nothing
if toRemove == nil {
return
}
if err := remove(im, toRemove); err != nil {
log.WithError(err).Error("could not remove emails")
}
}
// SelectFolder selects a folder
//
// This function replaces the SelectFolder function defined on *imap.Dialer, to
// use the SELECT command instead of the EXTRACT command.
func SelectFolder(im *imap.Dialer, folder string) error {
_, err := im.Exec(`SELECT "`+imap.AddSlashes.Replace(folder)+`"`, true, imap.RetryCount, nil)
if err != nil {
return err
}
im.Folder = folder
return nil
}
func valid(allowedDomains []string, email *imap.Email) bool {
for addr := range email.From {
ok := len(allowedDomains) == 0
for _, domain := range allowedDomains {
if strings.HasSuffix(addr, domain) {
ok = true
break
}
}
if !ok {
return false
}
}
return true
}
func remove(im *imap.Dialer, uids []int) error {
for _, uid := range uids {
_, err := im.Exec(fmt.Sprintf("UID STORE %d +FLAGS.SILENT (\\Deleted)", uid), false, imap.RetryCount, nil)
if err != nil {
return err
}
}
_, err := im.Exec("EXPUNGE", false, imap.RetryCount, nil)
return err
}
var converter = md.NewConverter("", true, &md.Options{
HeadingStyle: "setext",
StrongDelimiter: "**",
LinkStyle: "inlined",
})
// markdownBody takes a html string and converts it into a formatted markdown body.
//
// markdownBody also removes the signature and replaces double new lines with
// single new lines.
func markdownBody(raw string) (string, error) {
body, err := converter.ConvertString(raw)
if err != nil {
return "", fmt.Errorf("convert to html: %v", err)
}
signature := strings.LastIndex(body, "\\-\\-")
if signature != -1 {
body = body[:signature]
}
body = strings.ReplaceAll(body, "\n\n", "\n")
return strings.TrimSpace(body), nil
}
// buildContents splits body into chunks so each fit within maxLen.
//
// The size of any element Contents will never exceed maxLen. For the first
// item in Contents, Subject is also considered as part of the contents.
func buildContents(maxLen int, subject, body string) (contents []string) {
max := maxLen - len(subject)
for max < len(body) {
body = strings.TrimSpace(body)
i := strings.LastIndexAny(body[:max], "\n.!")
// there are no new lines in the first max len chars
if i == -1 {
i = max - 1
}
// use i+1 to include the last indexed character
contents = append(contents, body[:i+1])
body = body[i+1:]
max = maxLen
}
if body != "" {
contents = append(contents, body)
}
return contents
}