Files
jfa-go/email.go
Harvey Tindall 8781e48601 email: change font, template common parts
Using the newer Jellyfin logo font for the header and hanken grotesk for the body.
Tried to redo emails with maizzle because using tailwind sounded nice, but getting it
to look like a17t would be more trouble than it's worth, since you can't
use CSS vars in emails and a17t uses them heavily. Instead, cleaned up
the mj-header a little and stored it in a separate file, and also the
header & footer, and changed the template vars with {{ .header }}  and
{{ .footer }} for all emails. Values are determined by
CustomContentInfo.Header/FooterText funcs. nil values are replaced at
program start by _runtimeValidator.

also, i beg of you don't try to do light/dark mode with mjml, you'll
want to die.
2025-09-01 15:31:28 +01:00

731 lines
26 KiB
Go

package main
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"html/template"
"io"
"io/fs"
"maps"
"net/http"
"net/url"
"os"
"strconv"
"strings"
textTemplate "text/template"
"time"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/hrfee/jfa-go/easyproxy"
lm "github.com/hrfee/jfa-go/logmessages"
"github.com/hrfee/mediabrowser"
"github.com/itchyny/timefmt-go"
"github.com/mailgun/mailgun-go/v4"
"github.com/timshannon/badgerhold/v4"
sMail "github.com/xhit/go-simple-mail/v2"
)
var markdownRenderer = html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
// EmailClient implements email sending, right now via smtp, mailgun or a dummy client.
type EmailClient interface {
Send(fromName, fromAddr string, message *Message, address ...string) error
}
// Emailer contains the email sender, translations, and methods to construct messages.
type Emailer struct {
fromAddr, fromName string
lang emailLang
sender EmailClient
config *Config
storage *Storage
LoggerSet
}
// Message stores content.
type Message struct {
Subject string `json:"subject"`
HTML string `json:"html"`
Text string `json:"text"`
Markdown string `json:"markdown"`
}
func (emailer *Emailer) formatExpiry(expiry time.Time, tzaware bool) (d, t, expiresIn string) {
d = timefmt.Format(expiry, datePattern)
t = timefmt.Format(expiry, timePattern)
currentTime := time.Now()
if tzaware {
currentTime = currentTime.UTC()
}
_, _, days, hours, minutes, _ := timeDiff(expiry, currentTime)
if days != 0 {
expiresIn += strconv.Itoa(days) + "d "
}
if hours != 0 {
expiresIn += strconv.Itoa(hours) + "h "
}
if minutes != 0 {
expiresIn += strconv.Itoa(minutes) + "m "
}
expiresIn = strings.TrimSuffix(expiresIn, " ")
return
}
// NewEmailer configures and returns a new emailer.
func NewEmailer(config *Config, storage *Storage, logs LoggerSet) *Emailer {
emailer := &Emailer{
fromAddr: config.Section("email").Key("address").String(),
fromName: config.Section("email").Key("from").String(),
lang: storage.lang.Email[storage.lang.chosenEmailLang],
LoggerSet: logs,
config: config,
storage: storage,
}
method := emailer.config.Section("email").Key("method").String()
if method == "smtp" {
enc := sMail.EncryptionSTARTTLS
switch emailer.config.Section("smtp").Key("encryption").String() {
case "ssl_tls":
enc = sMail.EncryptionSSLTLS
case "starttls":
enc = sMail.EncryptionSTARTTLS
case "none":
enc = sMail.EncryptionNone
}
username := emailer.config.Section("smtp").Key("username").MustString("")
password := emailer.config.Section("smtp").Key("password").String()
if username == "" && password != "" {
username = emailer.fromAddr
}
authType := sMail.AuthType(emailer.config.Section("smtp").Key("auth_type").MustInt(4))
err := emailer.NewSMTP(emailer.config.Section("smtp").Key("server").String(), emailer.config.Section("smtp").Key("port").MustInt(465), username, password, enc, emailer.config.Section("smtp").Key("ssl_cert").MustString(""), emailer.config.Section("smtp").Key("hello_hostname").String(), emailer.config.Section("smtp").Key("cert_validation").MustBool(true), authType, emailer.config.proxyConfig)
if err != nil {
emailer.err.Printf(lm.FailedInitSMTP, err)
}
} else if method == "mailgun" {
emailer.NewMailgun(emailer.config.Section("mailgun").Key("api_url").String(), emailer.config.Section("mailgun").Key("api_key").String(), emailer.config.proxyTransport)
} else if method == "dummy" {
emailer.sender = &DummyClient{}
}
return emailer
}
// DummyClient just logs the email to the console for debugging purposes. It can be used by settings [email]/method to "dummy".
type DummyClient struct{}
func (dc *DummyClient) Send(fromName, fromAddr string, email *Message, address ...string) error {
fmt.Printf("FROM: %s <%s>\nTO: %s\nTEXT: %s\n", fromName, fromAddr, strings.Join(address, ", "), email.Text)
return nil
}
// SMTP supports SSL/TLS and STARTTLS; implements EmailClient.
type SMTP struct {
Client *sMail.SMTPServer
}
// NewSMTP returns an SMTP emailClient.
func (emailer *Emailer) NewSMTP(server string, port int, username, password string, encryption sMail.Encryption, certPath string, helloHostname string, validateCertificate bool, authType sMail.AuthType, proxy *easyproxy.ProxyConfig) (err error) {
sender := &SMTP{}
sender.Client = sMail.NewSMTPClient()
sender.Client.Encryption = encryption
if username != "" || password != "" {
sender.Client.Authentication = authType
sender.Client.Username = username
sender.Client.Password = password
}
sender.Client.Helo = helloHostname
sender.Client.ConnectTimeout, sender.Client.SendTimeout = 15*time.Second, 15*time.Second
sender.Client.Host = server
sender.Client.Port = port
sender.Client.KeepAlive = false
// x509.SystemCertPool is unavailable on windows
if PLATFORM == "windows" {
sender.Client.TLSConfig = &tls.Config{
InsecureSkipVerify: !validateCertificate,
ServerName: server,
}
if proxy != nil {
sender.Client.CustomConn, err = easyproxy.NewConn(*proxy, fmt.Sprintf("%s:%d", server, port), sender.Client.TLSConfig)
}
emailer.sender = sender
return
}
rootCAs, err := x509.SystemCertPool()
if rootCAs == nil || err != nil {
rootCAs = x509.NewCertPool()
}
if certPath != "" {
var cert []byte
cert, err = os.ReadFile(certPath)
if rootCAs.AppendCertsFromPEM(cert) == false {
err = errors.New("failed to append cert to pool")
}
}
sender.Client.TLSConfig = &tls.Config{
InsecureSkipVerify: !validateCertificate,
ServerName: server,
RootCAs: rootCAs,
}
if proxy != nil {
sender.Client.CustomConn, err = easyproxy.NewConn(*proxy, fmt.Sprintf("%s:%d", server, port), sender.Client.TLSConfig)
}
emailer.sender = sender
return
}
func (sm *SMTP) Send(fromName, fromAddr string, email *Message, address ...string) error {
from := fmt.Sprintf("%s <%s>", fromName, fromAddr)
var cli *sMail.SMTPClient
var err error
cli, err = sm.Client.Connect()
if err != nil {
return err
}
defer cli.Close()
e := sMail.NewMSG()
e.SetFrom(from)
e.SetSubject(email.Subject)
e.AddTo(address...)
e.SetBody(sMail.TextPlain, email.Text)
if email.HTML != "" {
e.AddAlternative(sMail.TextHTML, email.HTML)
}
err = e.Send(cli)
return err
}
// Mailgun client implements EmailClient.
type Mailgun struct {
client *mailgun.MailgunImpl
}
// NewMailgun returns a Mailgun emailClient.
func (emailer *Emailer) NewMailgun(url, key string, transport *http.Transport) {
sender := &Mailgun{
client: mailgun.NewMailgun(strings.Split(emailer.fromAddr, "@")[1], key),
}
if transport != nil {
cli := sender.client.Client()
cli.Transport = transport
}
// Mailgun client takes the base url, so we need to trim off the end (e.g 'v3/messages')
if strings.Contains(url, "messages") {
url = url[0:strings.LastIndex(url, "/")]
}
if strings.Contains(url, "v3") {
url = url[0:strings.LastIndex(url, "/")]
}
sender.client.SetAPIBase(url)
emailer.sender = sender
}
func (mg *Mailgun) Send(fromName, fromAddr string, email *Message, address ...string) error {
message := mg.client.NewMessage(
fmt.Sprintf("%s <%s>", fromName, fromAddr),
email.Subject,
email.Text,
)
for _, a := range address {
// Adding variable tells mailgun to do a batch send, so users don't see other recipients.
message.AddRecipientAndVariables(a, map[string]interface{}{"unique_id": a})
}
message.SetHtml(email.HTML)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, _, err := mg.client.Send(ctx, message)
return err
}
type templ interface {
Execute(wr io.Writer, data interface{}) error
}
func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomContent, data map[string]any) (*Message, error) {
msg := &Message{
Subject: contentInfo.Subject(emailer.config, &emailer.lang),
}
// Template the subject for bonus points
if subject, err := templateEmail(msg.Subject, contentInfo.Variables, contentInfo.Conditionals, data); err == nil {
msg.Subject = subject
}
if cc.Enabled {
// Use template email, rather than the built-in's email file.
contentInfo.SourceFile = customContent["TemplateEmail"].SourceFile
content, err := templateEmail(cc.Content, contentInfo.Variables, contentInfo.Conditionals, data)
if err != nil {
emailer.err.Printf(lm.FailedConstructCustomContent, msg.Subject, err)
return msg, err
}
html := markdown.ToHTML([]byte(content), nil, markdownRenderer)
text := stripMarkdown(content)
templateData := map[string]interface{}{
"text": template.HTML(html),
"plaintext": text,
"md": content,
}
data = templateData
}
var err error = nil
var tpl templ
msg.Text = ""
msg.Markdown = ""
msg.HTML = ""
data["header"] = contentInfo.HeaderText(emailer.config, &emailer.lang)
data["footer"] = contentInfo.FooterText(emailer.config, &emailer.lang)
var keys []string
plaintext := emailer.config.Section("email").Key("plaintext").MustBool(false)
if plaintext {
if telegramEnabled || discordEnabled {
keys = []string{"text"}
msg.Text, msg.Markdown = "", ""
} else {
keys = []string{"text"}
msg.Text = ""
}
} else {
if telegramEnabled || discordEnabled {
keys = []string{"html", "text", "markdown"}
} else {
keys = []string{"html", "text"}
}
}
for _, key := range keys {
var filesystem fs.FS
var fpath string
if key == "markdown" {
filesystem, fpath = emailer.config.GetPath(contentInfo.SourceFile.Section, contentInfo.SourceFile.SettingPrefix+"text")
} else {
filesystem, fpath = emailer.config.GetPath(contentInfo.SourceFile.Section, contentInfo.SourceFile.SettingPrefix+key)
}
if key == "html" {
tpl, err = template.ParseFS(filesystem, fpath)
} else {
tpl, err = textTemplate.ParseFS(filesystem, fpath)
}
if err != nil {
return msg, fmt.Errorf("error reading from fs path \"%s\": %v", fpath, err)
}
// For constructTemplate, if "md" is found in data it's used in stead of "text".
foundMarkdown := false
if key == "markdown" {
_, foundMarkdown = data["md"]
if foundMarkdown {
data["plaintext"], data["md"] = data["md"], data["plaintext"]
}
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, data)
if err != nil {
return msg, err
}
if foundMarkdown {
data["plaintext"], data["md"] = data["md"], data["plaintext"]
}
if key == "html" {
msg.HTML = tplData.String()
} else if key == "text" {
msg.Text = tplData.String()
} else {
msg.Markdown = tplData.String()
}
}
return msg, nil
}
func (emailer *Emailer) baseValues(name string, username string, placeholders bool, values map[string]any) (CustomContentInfo, map[string]any) {
contentInfo := customContent[name]
template := map[string]any{
"username": username,
}
maps.Copy(template, values)
// When generating a version for the user to customise, we'll replace "variable" with "{variable}", so the templater used for custom content understands them.
if placeholders {
for _, v := range contentInfo.Variables {
template[v] = "{" + v + "}"
}
}
return contentInfo, template
}
func (emailer *Emailer) constructConfirmation(code, username, key string, placeholders bool) (*Message, error) {
if placeholders {
username = "{username}"
}
contentInfo, template := emailer.baseValues("EmailConfirmation", username, placeholders, map[string]any{
"helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}),
"clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"),
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
"confirmEmail": emailer.lang.EmailConfirmation.get("confirmEmail"),
})
if !placeholders {
inviteLink := ExternalURI(nil)
if code == "" { // Personal email change
inviteLink = fmt.Sprintf("%s/my/confirm/%s", inviteLink, url.PathEscape(key))
} else { // Invite email confirmation
inviteLink = fmt.Sprintf("%s%s/%s?key=%s", inviteLink, PAGES.Form, code, url.PathEscape(key))
}
template["confirmationURL"] = inviteLink
}
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructInvite(invite Invite, placeholders bool) (*Message, error) {
expiry := invite.ValidTill
d, t, expiresIn := emailer.formatExpiry(expiry, false)
inviteLink := fmt.Sprintf("%s%s/%s", ExternalURI(nil), PAGES.Form, invite.Code)
contentInfo, template := emailer.baseValues("InviteEmail", "", placeholders, map[string]any{
"hello": emailer.lang.InviteEmail.get("hello"),
"youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"),
"toJoin": emailer.lang.InviteEmail.get("toJoin"),
"linkButton": emailer.lang.InviteEmail.get("linkButton"),
"date": d,
"time": t,
"expiresInMinutes": expiresIn,
"inviteURL": inviteLink,
"inviteExpiry": emailer.lang.InviteEmail.get("inviteExpiry"),
})
if !placeholders {
template["inviteExpiry"] = emailer.lang.InviteEmail.template("inviteExpiry", template)
}
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructExpiry(invite Invite, placeholders bool) (*Message, error) {
expiry := formatDatetime(invite.ValidTill)
contentInfo, template := emailer.baseValues("InviteExpiry", "", placeholders, map[string]any{
"inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"),
"expiredAt": emailer.lang.InviteExpiry.get("expiredAt"),
"code": "\"" + invite.Code + "\"",
"time": expiry,
})
if !placeholders {
template["expiredAt"] = emailer.lang.InviteExpiry.template("expiredAt", template)
}
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructCreated(username, address string, when time.Time, invite Invite, placeholders bool) (*Message, error) {
// NOTE: This was previously invite.Created, not sure why.
created := formatDatetime(when)
contentInfo, template := emailer.baseValues("UserCreated", username, placeholders, map[string]any{
"aUserWasCreated": emailer.lang.UserCreated.get("aUserWasCreated"),
"nameString": emailer.lang.Strings.get("name"),
"addressString": emailer.lang.Strings.get("emailAddress"),
"timeString": emailer.lang.UserCreated.get("time"),
"code": "\"" + invite.Code + "\"",
"name": username,
"time": created,
"address": address,
})
if !placeholders {
template["aUserWasCreated"] = emailer.lang.UserCreated.template("aUserWasCreated", template)
if emailer.config.Section("email").Key("no_username").MustBool(false) {
template["address"] = "n/a"
}
}
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructReset(pwr PasswordReset, placeholders bool) (*Message, error) {
if placeholders {
pwr.Username = "{username}"
}
d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true)
linkResetEnabled := emailer.config.Section("password_resets").Key("link_reset").MustBool(false)
contentInfo, template := emailer.baseValues("PasswordReset", pwr.Username, placeholders, map[string]any{
"helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": pwr.Username}),
"someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"),
"ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"),
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
"pinString": emailer.lang.PasswordReset.get("pin"),
"codeExpiry": emailer.lang.PasswordReset.get("codeExpiry"),
"link_reset": linkResetEnabled && !placeholders,
"date": d,
"time": t,
"expiresInMinutes": expiresIn,
"pin": pwr.Pin,
})
if linkResetEnabled {
template["ifItWasYou"] = emailer.lang.PasswordReset.get("ifItWasYouLink")
}
if !placeholders {
template["codeExpiry"] = emailer.lang.PasswordReset.template("codeExpiry", template)
if linkResetEnabled {
pinLink, err := GenResetLink(pwr.Pin)
if err != nil {
template["link_reset"] = false
emailer.info.Printf(lm.FailedGeneratePWRLink, err)
} else {
template["pin"] = pinLink
// Only used in html email.
template["pin_code"] = pwr.Pin
}
}
}
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructDeleted(username, reason string, placeholders bool) (*Message, error) {
if placeholders {
username = "{username}"
reason = "{reason}"
}
contentInfo, template := emailer.baseValues("UserDeleted", username, placeholders, map[string]any{
"helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}),
"yourAccountWas": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
"reasonString": emailer.lang.Strings.get("reason"),
"reason": reason,
})
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructDisabled(username, reason string, placeholders bool) (*Message, error) {
if placeholders {
username = "{username}"
reason = "{reason}"
}
contentInfo, template := emailer.baseValues("UserDisabled", username, placeholders, map[string]any{
"helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}),
"yourAccountWas": emailer.lang.UserDisabled.get("yourAccountWasDisabled"),
"reasonString": emailer.lang.Strings.get("reason"),
"reason": reason,
})
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructEnabled(username, reason string, placeholders bool) (*Message, error) {
if placeholders {
username = "{username}"
reason = "{reason}"
}
contentInfo, template := emailer.baseValues("UserEnabled", username, placeholders, map[string]any{
"helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}),
"yourAccountWas": emailer.lang.UserEnabled.get("yourAccountWasEnabled"),
"reasonString": emailer.lang.Strings.get("reason"),
"reason": reason,
})
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructExpiryAdjusted(username string, expiry time.Time, reason string, placeholders bool) (*Message, error) {
if placeholders {
username = "{username}"
}
exp := formatDatetime(expiry)
contentInfo, template := emailer.baseValues("UserExpiryAdjusted", username, placeholders, map[string]any{
"helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}),
"yourExpiryWasAdjusted": emailer.lang.UserExpiryAdjusted.get("yourExpiryWasAdjusted"),
"ifPreviouslyDisabled": emailer.lang.UserExpiryAdjusted.get("ifPreviouslyDisabled"),
"reasonString": emailer.lang.Strings.get("reason"),
"reason": reason,
"newExpiry": exp,
})
cc := emailer.storage.MustGetCustomContentKey("UserExpiryAdjusted")
if !placeholders {
if !cc.Enabled {
template["newExpiry"] = emailer.lang.UserExpiryAdjusted.template("newExpiry", tmpl{
"date": exp,
})
}
}
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructExpiryReminder(username string, expiry time.Time, placeholders bool) (*Message, error) {
if placeholders {
username = "{username}"
}
d, t, expiresIn := emailer.formatExpiry(expiry, false)
contentInfo, template := emailer.baseValues("ExpiryReminder", username, placeholders, map[string]any{
"helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}),
"yourAccountIsDueToExpire": emailer.lang.ExpiryReminder.get("yourAccountIsDueToExpire"),
"expiresIn": expiresIn,
"date": d,
"time": t,
})
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
if !placeholders {
if !cc.Enabled && !expiry.IsZero() {
template["yourAccountIsDueToExpire"] = emailer.lang.ExpiryReminder.template("yourAccountIsDueToExpire", template)
}
}
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructWelcome(username string, expiry time.Time, placeholders bool) (*Message, error) {
var exp any = formatDatetime(expiry)
if placeholders {
username = "{username}"
exp = "{yourAccountWillExpire}"
}
contentInfo, template := emailer.baseValues("WelcomeEmail", username, placeholders, map[string]any{
"welcome": emailer.lang.WelcomeEmail.get("welcome"),
"youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"),
"jellyfinURLString": emailer.lang.WelcomeEmail.get("jellyfinURL"),
"jellyfinURL": emailer.config.Section("jellyfin").Key("public_server").String(),
"usernameString": emailer.lang.Strings.get("username"),
})
if !expiry.IsZero() || placeholders {
template["yourAccountWillExpire"] = emailer.lang.WelcomeEmail.template("yourAccountWillExpire", tmpl{
"date": exp,
})
}
cc := emailer.storage.MustGetCustomContentKey("WelcomeEmail")
if !placeholders {
if cc.Enabled && !expiry.IsZero() {
template["yourAccountWillExpire"] = exp
}
}
return emailer.construct(contentInfo, cc, template)
}
func (emailer *Emailer) constructUserExpired(username string, placeholders bool) (*Message, error) {
contentInfo, template := emailer.baseValues("UserExpired", username, placeholders, map[string]any{
"yourAccountHasExpired": emailer.lang.UserExpired.get("yourAccountHasExpired"),
"contactTheAdmin": emailer.lang.UserExpired.get("contactTheAdmin"),
})
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
return emailer.construct(contentInfo, cc, template)
}
// calls the send method in the underlying emailClient.
func (emailer *Emailer) send(email *Message, address ...string) error {
return emailer.sender.Send(emailer.fromName, emailer.fromAddr, email, address...)
}
func (app *appContext) sendByID(email *Message, ID ...string) (err error) {
for _, id := range ID {
if tgChat, ok := app.storage.GetTelegramKey(id); ok && tgChat.Contact && telegramEnabled {
err = app.telegram.Send(email, tgChat.ChatID)
// if err != nil {
// return err
// }
}
if dcChat, ok := app.storage.GetDiscordKey(id); ok && dcChat.Contact && discordEnabled {
err = app.discord.Send(email, dcChat.ChannelID)
// if err != nil {
// return err
// }
}
if mxChat, ok := app.storage.GetMatrixKey(id); ok && mxChat.Contact && matrixEnabled {
err = app.matrix.Send(email, mxChat)
// if err != nil {
// return err
// }
}
if address, ok := app.storage.GetEmailsKey(id); ok && address.Contact && emailEnabled {
err = app.email.send(email, address.Addr)
// if err != nil {
// return err
// }
}
// if err != nil {
// return err
// }
}
return
}
func (app *appContext) getAddressOrName(jfID string) string {
if dcChat, ok := app.storage.GetDiscordKey(jfID); ok && dcChat.Contact && discordEnabled {
return RenderDiscordUsername(dcChat)
}
if tgChat, ok := app.storage.GetTelegramKey(jfID); ok && tgChat.Contact && telegramEnabled {
return "@" + tgChat.Username
}
if addr, ok := app.storage.GetEmailsKey(jfID); ok {
return addr.Addr
}
if mxChat, ok := app.storage.GetMatrixKey(jfID); ok && mxChat.Contact && matrixEnabled {
return mxChat.UserID
}
return ""
}
// ReverseUserSearch returns the jellyfin ID of the user with the given username, email, or contact method username.
// returns "" if none found. returns only the first match, might be an issue if there are users with the same contact method usernames.
func (app *appContext) ReverseUserSearch(address string, matchUsername, matchEmail, matchContactMethod bool) (user mediabrowser.User, ok bool) {
ok = false
var err error = nil
if matchUsername {
user, err = app.jf.UserByName(address, false)
if err == nil {
ok = true
return
}
}
if matchEmail {
emailAddresses := []EmailAddress{}
err = app.storage.db.Find(&emailAddresses, badgerhold.Where("Addr").Eq(address))
if err == nil && len(emailAddresses) > 0 {
for _, emailUser := range emailAddresses {
user, err = app.jf.UserByID(emailUser.JellyfinID, false)
if err == nil {
ok = true
return
}
}
}
}
// Dont know how we'd use badgerhold when we need to render each username,
// Apart from storing the rendered name in the db.
if matchContactMethod {
for _, dcUser := range app.storage.GetDiscord() {
if RenderDiscordUsername(dcUser) == strings.ToLower(address) {
user, err = app.jf.UserByID(dcUser.JellyfinID, false)
if err == nil {
ok = true
return
}
}
}
tgUsername := strings.TrimPrefix(address, "@")
telegramUsers := []TelegramUser{}
err = app.storage.db.Find(&telegramUsers, badgerhold.Where("Username").Eq(tgUsername))
if err == nil && len(telegramUsers) > 0 {
for _, telegramUser := range telegramUsers {
user, err = app.jf.UserByID(telegramUser.JellyfinID, false)
if err == nil {
ok = true
return
}
}
}
matrixUsers := []MatrixUser{}
err = app.storage.db.Find(&matrixUsers, badgerhold.Where("UserID").Eq(address))
if err == nil && len(matrixUsers) > 0 {
for _, matrixUser := range matrixUsers {
user, err = app.jf.UserByID(matrixUser.JellyfinID, false)
if err == nil {
ok = true
return
}
}
}
}
return
}
// EmailAddressExists returns whether or not a user with the given email address exists.
func (app *appContext) EmailAddressExists(address string) bool {
c, err := app.storage.db.Count(&EmailAddress{}, badgerhold.Where("Addr").Eq(address))
return err != nil || c > 0
}