// Anope IRC Services // // Copyright (C) 2003-2025 Anope Contributors // // Anope is free software. You can use, modify, and/or distribute it under the // terms of version 2 of the GNU General Public License. See docs/LICENSE.txt // for the complete terms of this license and docs/AUTHORS.txt for a list of // contributors. // // Based on the original code of Epona by Lara // Based on the original code of Services by Andy Church // // SPDX-License-Identifier: GPL-2.0-only #include "module.h" namespace { bool clean = false; unsigned maxemails = 0; /* strip dots from username, and remove anything after the first + */ Anope::string CleanMail(const Anope::string &email) { size_t host = email.find('@'); if (host == Anope::string::npos) return email; Anope::string username = email.substr(0, host); username = username.replace_all_cs(".", ""); size_t sz = username.find('+'); if (sz != Anope::string::npos) username = username.substr(0, sz); Anope::string cleaned = username + email.substr(host); Log(LOG_DEBUG) << "cleaned " << email << " to " << cleaned; return cleaned; } unsigned CountEmail(const Anope::string &email, NickCore *unc) { unsigned count = 0; if (email.empty()) return 0; Anope::string cleanemail = clean ? CleanMail(email) : email; for (const auto &[_, nc] : *NickCoreList) { Anope::string cleannc = clean ? CleanMail(nc->email) : nc->email; if (unc != nc && cleanemail.equals_ci(cleannc)) ++count; } return count; } bool CheckLimitReached(CommandSource &source, const Anope::string &email, bool ignoreself) { if (!maxemails || email.empty()) return false; if (CountEmail(email, ignoreself ? source.GetAccount() : NULL) < maxemails) return false; source.Reply(maxemails, N_("The email address \002%s\002 has reached its usage limit of %u user.", "The email address \002%s\002 has reached its usage limit of %u users."), email.c_str(), maxemails); return true; } } struct EmailChange final { Anope::string code; Anope::string email; time_t requested = Anope::CurTime; }; class CommandNSConfirmEmail final : public Command { private: PrimitiveExtensibleItem &ns_set_email; public: CommandNSConfirmEmail(Module *creator, PrimitiveExtensibleItem &nse) : Command(creator, "nickserv/confirm/email", 1, 2) , ns_set_email(nse) { this->SetDesc(_("Confirm a previous change of email address")); this->SetSyntax(_("\037code\037")); this->SetSyntax(_("@\037nickname\037"), [](auto &source) { return source.HasPriv("nickserv/confirm/email"); }); } void Execute(CommandSource &source, const std::vector ¶ms) override { auto has_priv = source.HasPriv("nickserv/confirm/email"); Anope::string code; NickAlias *na; if (params[0] == '@') { if (!has_priv) { source.Reply(ACCESS_DENIED); return; } auto nick = params[0].substr(0); na = NickAlias::Find(nick); if (!na) { source.Reply(NICK_X_NOT_REGISTERED, nick.c_str()); return; } } else { code = params[0]; na = source.GetAccount()->na; } NickCore *nc = na->nc; if (nc->HasExt("NS_SUSPENDED")) { source.Reply(NICK_X_SUSPENDED, na->nick.c_str()); return; } auto *nse = ns_set_email.Get(nc); if (!nse) { source.Reply(_("There is no email address change confirmation pending for %s."), na->nick.c_str()); return; } if (!has_priv) { if (!code.equals_cs(nse->code)) { source.Reply(_("The email address change confirmation code you specified for %s is incorrect."), na->nick.c_str()); return; } auto changeexpire = Config->GetModule(owner).Get("changeexpire", "1d"); if (nse->requested < Anope::CurTime - changeexpire) { ns_set_email.Unset(nc); source.Reply(_("The email address change request for %s has expired."), na->nick.c_str()); return; } } if (!CheckLimitReached(source, nse->email, true)) { ns_set_email.Unset(nc); return; } auto old_email = nc->email; nc->email = nse->email; ns_set_email.Unset(nc); Log(nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to confirm the email address change of " << nc->display << " from " << old_email << " to " << nc->email; source.Reply(_("The email address of %s has been changed from \002%s\002 to \002%s\002."), na->nick.c_str(), old_email.c_str(), nc->email.c_str()); } bool OnHelp(CommandSource &source, const Anope::string &) override { auto changeexpire = Config->GetModule(owner).Get("changeexpire", "1d"); this->SendSyntax(source); source.Reply(" "); source.Reply(_( "Confirms an change of email address. You have %s after requesting an email " "address change to do this before your request expires." ), Anope::Duration(changeexpire, source.GetAccount()).c_str()); if (source.HasPriv("nickserv/confirm/email")) { source.Reply(" "); source.Reply(_( "Additionally, Services Operators with the \037nickserv/confirm/email\037 " "permission can specify @\037nickname\037 instead of \037code\037 to force " "confirm another user's change of email address." )); } return true; } }; class CommandNSGetEmail final : public Command { public: CommandNSGetEmail(Module *creator) : Command(creator, "nickserv/getemail", 1, 1) { this->SetDesc(_("Matches and returns all users that registered using given email address")); this->SetSyntax(_("\037email\037")); } void Execute(CommandSource &source, const std::vector ¶ms) override { const Anope::string &email = params[0]; int j = 0; Log(LOG_ADMIN, source, this) << "on " << email; for (const auto &[_, nc] : *NickCoreList) { if (!nc->email.empty() && Anope::Match(nc->email, email)) { ++j; source.Reply(_("Email matched: \002%s\002 (\002%s\002) to \002%s\002."), nc->display.c_str(), nc->email.c_str(), email.c_str()); } } if (j <= 0) { source.Reply(_("No registrations matching \002%s\002 were found."), email.c_str()); return; } } bool OnHelp(CommandSource &source, const Anope::string &subcommand) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_("Returns the matching accounts that used given email.")); return true; } }; class CommandNSSetEmail : public Command { static bool SendConfirmMail(User *u, NickCore *nc, BotInfo *bi, const Anope::string &new_email) { auto *nse = nc->Extend("ns_set_email"); nse->code = Anope::Random(Config->GetBlock("options").Get("codelength", "15")); nse->email = new_email; Anope::map vars = { { "old_email", nc->email }, { "new_email", new_email }, { "account", nc->display }, { "network", Config->GetBlock("networkinfo").Get("networkname") }, { "code", nse->code }, }; auto subject = Anope::Template(Config->GetBlock("mail").Get("emailchange_subject"), vars); auto message = Anope::Template(Config->GetBlock("mail").Get("emailchange_message"), vars); Anope::string old = nc->email; nc->email = new_email; bool b = Mail::Send(u, nc, bi, subject, message); nc->email = old; return b; } public: CommandNSSetEmail(Module *creator, const Anope::string &cname = "nickserv/set/email", size_t min = 0) : Command(creator, cname, min, min + 1) { this->SetDesc(_("Associate an email address with your nickname")); this->SetSyntax(_("\037address\037")); } void Run(CommandSource &source, const Anope::string &user, const Anope::string ¶m) { if (Anope::ReadOnly) { source.Reply(READ_ONLY_MODE); return; } const NickAlias *na = NickAlias::Find(user); if (!na) { source.Reply(NICK_X_NOT_REGISTERED, user.c_str()); return; } NickCore *nc = na->nc; if (nc->HasExt("UNCONFIRMED")) { source.Reply(_("You may not change the email address of an unconfirmed account.")); return; } if (param.empty() && Config->GetModule("nickserv").Get("forceemail", "yes")) { source.Reply(_("You cannot unset the email address on this network.")); return; } else if (Config->GetModule("nickserv").Get("secureadmins", "yes") && source.nc != nc && nc->IsServicesOper()) { source.Reply(_("You may not change the email address of other Services Operators.")); return; } else if (!param.empty() && !Mail::Validate(param)) { source.Reply(MAIL_X_INVALID, param.c_str()); return; } else if (CheckLimitReached(source, param, true)) return; EventReturn MOD_RESULT; FOREACH_RESULT(OnSetNickOption, MOD_RESULT, (source, this, nc, param)); if (MOD_RESULT == EVENT_STOP) return; const auto nsmailreg = Config->GetModule("ns_register").Get("registration").equals_ci("mail"); if (!param.empty() && Config->GetModule("nickserv").Get("confirmemailchanges", nsmailreg ? "yes" : "no") && source.GetAccount() == nc) { if (SendConfirmMail(source.GetUser(), source.GetAccount(), source.service, param)) { Log(LOG_COMMAND, source, this) << "to request changing the email address of " << nc->display << " to " << param; source.Reply(_("A confirmation email has been sent to \002%s\002. Follow the instructions in it to change your email address."), param.c_str()); } } else { if (!param.empty()) { Log(nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to change the email address of " << nc->display << " to " << param; nc->email = param; source.Reply(_("Email address for \002%s\002 changed to \002%s\002."), nc->display.c_str(), param.c_str()); } else { Log(nc == source.GetAccount() ? LOG_COMMAND : LOG_ADMIN, source, this) << "to unset the email address of " << nc->display; nc->email.clear(); source.Reply(_("Email address for \002%s\002 unset."), nc->display.c_str()); } } } void Execute(CommandSource &source, const std::vector ¶ms) override { this->Run(source, source.nc->display, params.size() ? params[0] : ""); } bool OnHelp(CommandSource &source, const Anope::string &) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_( "Associates the given email address with your nickname. " "This address will be displayed whenever someone requests " "information on the nickname with the \002INFO\002 command." )); return true; } }; class CommandNSSASetEmail final : public CommandNSSetEmail { public: CommandNSSASetEmail(Module *creator) : CommandNSSetEmail(creator, "nickserv/saset/email", 2) { this->ClearSyntax(); this->SetSyntax(_("\037nickname\037 \037address\037")); } void Execute(CommandSource &source, const std::vector ¶ms) override { this->Run(source, params[0], params.size() > 1 ? params[1] : ""); } bool OnHelp(CommandSource &source, const Anope::string &) override { this->SendSyntax(source); source.Reply(" "); source.Reply(_("Associates the given email address with the nickname.")); return true; } }; class NSEmail final : public Module { private: CommandNSConfirmEmail commandnsconfirmemail; CommandNSGetEmail commandnsgetemail; CommandNSSetEmail commandnssetemail; CommandNSSASetEmail commandnssasetemail; /* email, passcode */ PrimitiveExtensibleItem ns_set_email; public: NSEmail(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, VENDOR) , commandnsconfirmemail(this, ns_set_email) , commandnsgetemail(this) , commandnssetemail(this) , commandnssasetemail(this) , ns_set_email(this, "ns_set_email") { } void OnReload(Configuration::Conf &conf) override { const auto &modconf = conf.GetModule(this); maxemails = modconf.Get("maxemails"); clean = modconf.Get("remove_aliases", "yes"); } EventReturn OnPreCommand(CommandSource &source, Command *command, std::vector ¶ms) override { if (!source.IsOper() && command->name == "nickserv/register") { if (CheckLimitReached(source, params.size() > 1 ? params[1] : "", false)) return EVENT_STOP; } else if (!source.IsOper() && command->name == "nickserv/ungroup" && source.GetAccount()) { if (CheckLimitReached(source, source.GetAccount()->email, false)) return EVENT_STOP; } return EVENT_CONTINUE; } }; MODULE_INIT(NSEmail)