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" sTemplate "github.com/hrfee/simple-template" "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 := sTemplate.Template(msg.Subject, 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 := sTemplate.Template(cc.Content, 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 }