Split sendmail into common and sendmail-specific portions.
[srvx.git] / src / nickserv.c
index fab3460623a908c13fae5854b03fb14425d0d296..bf22dd65f58553f3ff3c5e02fa805204be2c2244 100644 (file)
@@ -1,5 +1,5 @@
 /* nickserv.c - Nick/authentication service
- * Copyright 2000-2004 srvx Development Team
+ * Copyright 2000-2006 srvx Development Team
  *
  * This file is part of srvx.
  *
 #include "modcmd.h"
 #include "opserv.h" /* for gag_create(), opserv_bad_channel() */
 #include "saxdb.h"
-#include "sendmail.h"
+#include "mail.h"
 #include "timeq.h"
 
 #ifdef HAVE_REGEX_H
-#include <regex.h>
+# include <regex.h>
+#else
+# include "rx/rxposix.h"
 #endif
 
 #define NICKSERV_CONF_NAME "services/nickserv"
@@ -47,6 +49,9 @@
 #define KEY_DB_BACKUP_FREQ "db_backup_freq"
 #define KEY_MODOPER_LEVEL "modoper_level"
 #define KEY_SET_EPITHET_LEVEL "set_epithet_level"
+#define KEY_SET_TITLE_LEVEL "set_title_level"
+#define KEY_SET_FAKEHOST_LEVEL "set_fakehost_level"
+#define KEY_TITLEHOST_SUFFIX "titlehost_suffix"
 #define KEY_FLAG_LEVELS "flag_levels"
 #define KEY_HANDLE_EXPIRE_FREQ "handle_expire_freq"
 #define KEY_ACCOUNT_EXPIRE_FREQ "account_expire_freq"
@@ -93,6 +98,7 @@
 #define KEY_TABLE_WIDTH "table_width"
 #define KEY_ANNOUNCEMENTS "announcements"
 #define KEY_MAXLOGINS "maxlogins"
+#define KEY_FAKEHOST "fakehost"
 
 #define NICKSERV_VALID_CHARS   "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
 
@@ -157,11 +163,12 @@ static const struct message_entry msgtab[] = {
     { "NSMSG_MUST_TIME_OUT", "You must wait for cookies of that type to time out." },
     { "NSMSG_ATE_COOKIE", "I ate the cookie for your account.  You may now have another." },
     { "NSMSG_USE_RENAME", "You are already authenticated to account $b%s$b -- contact the support staff to rename your account." },
+    { "NSMSG_ALREADY_REGISTERING", "You have already used $bREGISTER$b once this session; you may not use it again." },
     { "NSMSG_REGISTER_BAD_NICKMASK", "Could not recognize $b%s$b as either a current nick or a hostmask." },
     { "NSMSG_NICK_NOT_REGISTERED", "Nick $b%s$b has not been registered to any account." },
     { "NSMSG_HANDLE_NOT_FOUND", "Could not find your account -- did you register yet?" },
     { "NSMSG_ALREADY_AUTHED", "You are already authed to account $b%s$b; you must reconnect to auth to a different account." },
-    { "NSMSG_USE_AUTHCOOKIE", "Your hostmask is not valid for account $b%s$b.  Please use the $bauthcookie$b command to grant yourself access.  (/msg $S authcookie %s)" },
+    { "NSMSG_USE_AUTHCOOKIE", "Your hostmask is not valid for account $b%1$s$b.  Please use the $bauthcookie$b command to grant yourself access.  (/msg $S authcookie %1$s)" },
     { "NSMSG_HOSTMASK_INVALID", "Your hostmask is not valid for account $b%s$b." },
     { "NSMSG_USER_IS_SERVICE", "$b%s$b is a network service; you can only use that command on real users." },
     { "NSMSG_USER_PREV_AUTH", "$b%s$b is already authenticated." },
@@ -173,6 +180,9 @@ static const struct message_entry msgtab[] = {
     { "NSMSG_STAMPED_AUTH", "You have already authenticated to an account once this session; you may not authenticate to another." },
     { "NSMSG_STAMPED_RESETPASS", "You have already authenticated to an account once this session; you may not reset your password to authenticate again." },
     { "NSMSG_STAMPED_AUTHCOOKIE",  "You have already authenticated to an account once this session; you may not use a cookie to authenticate to another account." },
+    { "NSMSG_TITLE_INVALID", "Titles cannot contain any dots; please choose another." },
+    { "NSMSG_TITLE_TRUNCATED", "That title combined with the user's account name would result in a truncated host; please choose a shorter title." },
+    { "NSMSG_FAKEHOST_INVALID", "Fake hosts must be shorter than %d characters and cannot start with a dot." },
     { "NSMSG_HANDLEINFO_ON", "Account information for $b%s$b:" },
     { "NSMSG_HANDLEINFO_ID", "  Account ID: %lu" },
     { "NSMSG_HANDLEINFO_REGGED", "  Registered on: %s" },
@@ -188,6 +198,7 @@ static const struct message_entry msgtab[] = {
     { "NSMSG_HANDLEINFO_INFOLINE", "  Infoline: %s" },
     { "NSMSG_HANDLEINFO_FLAGS", "  Flags: %s" },
     { "NSMSG_HANDLEINFO_EPITHET", "  Epithet: %s" },
+    { "NSMSG_HANDLEINFO_FAKEHOST", "  Fake host: %s" },
     { "NSMSG_HANDLEINFO_LAST_HOST", "  Last quit hostmask: %s" },
     { "NSMSG_HANDLEINFO_LAST_HOST_UNKNOWN", "  Last quit hostmask: Unknown" },
     { "NSMSG_HANDLEINFO_NICKS", "  Nickname(s): %s" },
@@ -262,7 +273,7 @@ static const struct message_entry msgtab[] = {
     { "NSMSG_CLONE_AUTH", "Warning: %s (%s@%s) authed to your account." },
     { "NSMSG_SETTING_LIST", "$b$N account settings:$b" },
     { "NSMSG_INVALID_OPTION", "$b%s$b is an invalid account setting." },
-    { "NSMSG_INVALID_ANNOUNCE", "$b%s$b is an announcements value." },
+    { "NSMSG_INVALID_ANNOUNCE", "$b%s$b is an invalid announcements value." },
     { "NSMSG_SET_INFO", "$bINFO:         $b%s" },
     { "NSMSG_SET_WIDTH", "$bWIDTH:        $b%d" },
     { "NSMSG_SET_TABLEWIDTH", "$bTABLEWIDTH:   $b%d" },
@@ -277,17 +288,19 @@ static const struct message_entry msgtab[] = {
     { "NSMSG_SET_LANGUAGE", "$bLANGUAGE:     $b%s" },
     { "NSMSG_SET_LEVEL", "$bLEVEL:        $b%d" },
     { "NSMSG_SET_EPITHET", "$bEPITHET:      $b%s" },
+    { "NSMSG_SET_TITLE", "$bTITLE:        $b%s" },
+    { "NSMSG_SET_FAKEHOST", "$bFAKEHOST:    $b%s" },
     { "NSEMAIL_ACTIVATION_SUBJECT", "Account verification for %s" },
-    { "NSEMAIL_ACTIVATION_BODY", "This email has been sent to verify that this email address belongs to the person who tried to register an account on %1$s.  Your cookie is:\n    %2$s\nTo verify your email address and complete the account registration, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s %2$s\nIf you did NOT request this account, you do not need to do anything.  Please contact the %1$s staff if you have questions." },
+    { "NSEMAIL_ACTIVATION_BODY", "This email has been sent to verify that this email address belongs to the person who tried to register an account on %1$s.  Your cookie is:\n    %2$s\nTo verify your email address and complete the account registration, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s %2$s\nThis command is only used once to complete your account registration, and never again. Once you have run this command, you will need to authenticate everytime you reconnect to the network. To do this, you will have to type this command every time you reconnect:\n    /msg %3$s@%4$s AUTH %5$s your-password\n Please remember to fill in 'your-password' with the actual password you gave to us when you registered.\n\nIf you did NOT request this account, you do not need to do anything.  Please contact the %1$s staff if you have questions, and be sure to check our website." },
     { "NSEMAIL_PASSWORD_CHANGE_SUBJECT", "Password change verification on %s" },
     { "NSEMAIL_PASSWORD_CHANGE_BODY", "This email has been sent to verify that you wish to change the password on your account %5$s.  Your cookie is %2$s.\nTo complete the password change, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s %2$s\nIf you did NOT request your password to be changed, you do not need to do anything.  Please contact the %1$s staff if you have questions." },
     { "NSEMAIL_EMAIL_CHANGE_SUBJECT", "Email address change verification for %s" },
     { "NSEMAIL_EMAIL_CHANGE_BODY_NEW", "This email has been sent to verify that your email address belongs to the same person as account %5$s on %1$s.  The SECOND HALF of your cookie is %2$.*6$s.\nTo verify your address as associated with this account, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s ?????%2$.*6$s\n(Replace the ????? with the FIRST HALF of the cookie, as sent to your OLD email address.)\nIf you did NOT request this email address to be associated with this account, you do not need to do anything.  Please contact the %1$s staff if you have questions." },
     { "NSEMAIL_EMAIL_CHANGE_BODY_OLD", "This email has been sent to verify that you want to change your email for account %5$s on %1$s from this address to %7$s.  The FIRST HALF of your cookie is %2$.*6$s\nTo verify your new address as associated with this account, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s %2$.*6$s?????\n(Replace the ????? with the SECOND HALF of the cookie, as sent to your NEW email address.)\nIf you did NOT request this change of email address, you do not need to do anything.  Please contact the %1$s staff if you have questions." },
     { "NSEMAIL_EMAIL_VERIFY_SUBJECT", "Email address verification for %s" },
-    { "NSEMAIL_EMAIL_VERIFY_BODY", "This email has been sent to verify that this address belongs to the same person as %5$s on %1$s.  Your cookie is %2$s.\nTo verify your address as associated with this account, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s %1$s\nIf you did NOT request this email address to be associated with this account, you do not need to do anything.  Please contact the %1$s staff if you have questions." },
+    { "NSEMAIL_EMAIL_VERIFY_BODY", "This email has been sent to verify that this address belongs to the same person as %5$s on %1$s.  Your cookie is %2$s.\nTo verify your address as associated with this account, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s %2$s\nIf you did NOT request this email address to be associated with this account, you do not need to do anything.  Please contact the %1$s staff if you have questions." },
     { "NSEMAIL_ALLOWAUTH_SUBJECT", "Authentication allowed for %s" },
-    { "NSEMAIL_ALLOWAUTH_BODY", "This email has been sent to let you authenticate (auth) to account %5$s on %1$s.  Your cookie is %2$s.\nTo auth to that account, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s %1$s\nIf you did NOT request this authorization, you do not need to do anything.  Please contact the %1$s staff if you have questions." },
+    { "NSEMAIL_ALLOWAUTH_BODY", "This email has been sent to let you authenticate (auth) to account %5$s on %1$s.  Your cookie is %2$s.\nTo auth to that account, log on to %1$s and type the following command:\n    /msg %3$s@%4$s COOKIE %5$s %2$s\nIf you did NOT request this authorization, you do not need to do anything.  Please contact the %1$s staff if you have questions." },
     { "CHECKPASS_YES", "Yes." },
     { "CHECKPASS_NO", "No." },
     { NULL, NULL }
@@ -301,6 +314,7 @@ enum reclaim_action {
 };
 static void nickserv_reclaim(struct userNode *user, struct nick_info *ni, enum reclaim_action action);
 static void nickserv_reclaim_p(void *data);
+static int nickserv_addmask(struct userNode *user, struct handle_info *hi, const char *mask);
 
 static struct {
     unsigned int disable_nicks : 1;
@@ -326,6 +340,8 @@ static struct {
     unsigned long nochan_handle_expire_delay;
     unsigned long modoper_level;
     unsigned long set_epithet_level;
+    unsigned long set_title_level;
+    unsigned long set_fakehost_level;
     unsigned long handles_per_email;
     unsigned long email_search_level;
     const char *network_name;
@@ -415,13 +431,6 @@ register_nick(const char *nick, struct handle_info *owner)
     dict_insert(nickserv_nick_dict, ni->nick, ni);
 }
 
-static void
-free_nick_info(void *vni)
-{
-    struct nick_info *ni = vni;
-    free(ni);
-}
-
 static void
 delete_nick(struct nick_info *ni)
 {
@@ -493,6 +502,7 @@ free_handle_info(void *vhi)
         delete_nick(hi->nicks);
     free(hi->infoline);
     free(hi->epithet);
+    free(hi->fakehost);
     if (hi->cookie) {
         timeq_del(hi->cookie->expires, nickserv_free_cookie, hi->cookie, 0);
         nickserv_free_cookie(hi->cookie);
@@ -673,7 +683,10 @@ smart_get_handle_info(struct userNode *service, struct userNode *user, const cha
             return 0;
         }
         if (IsLocal(target)) {
-            send_message(user, service, "NSMSG_USER_IS_SERVICE", target->nick);
+           if (IsService(target))
+                send_message(user, service, "NSMSG_USER_IS_SERVICE", target->nick);
+           else
+                send_message(user, service, "MSG_USER_AUTHENTICATE", target->nick);
             return 0;
         }
         if (!(hi = target->handle_info)) {
@@ -740,6 +753,8 @@ is_secure_password(const char *handle, const char *pass, struct userNode *user)
 {
     unsigned int i, len;
     unsigned int cnt_digits = 0, cnt_upper = 0, cnt_lower = 0;
+    int p;
+
     len = strlen(pass);
     if (len < nickserv_conf.password_min_length) {
         if (user)
@@ -751,8 +766,8 @@ is_secure_password(const char *handle, const char *pass, struct userNode *user)
             send_message(user, nickserv, "NSMSG_PASSWORD_ACCOUNT");
         return 0;
     }
-    dict_find(nickserv_conf.weak_password_dict, pass, &i);
-    if (i) {
+    dict_find(nickserv_conf.weak_password_dict, pass, &p);
+    if (p) {
         if (user)
             send_message(user, nickserv, "NSMSG_PASSWORD_DICTIONARY");
         return 0;
@@ -811,6 +826,36 @@ reg_handle_rename_func(handle_rename_func_t func)
     rf_list[rf_list_used++] = func;
 }
 
+static char *
+generate_fakehost(struct handle_info *handle)
+{
+    extern const char *hidden_host_suffix;
+    static char buffer[HOSTLEN+1];
+
+    if (!handle->fakehost) {
+        snprintf(buffer, sizeof(buffer), "%s.%s", handle->handle, hidden_host_suffix);
+        return buffer;
+    } else if (handle->fakehost[0] == '.') {
+        /* A leading dot indicates the stored value is actually a title. */
+        snprintf(buffer, sizeof(buffer), "%s.%s.%s", handle->handle, handle->fakehost+1, nickserv_conf.titlehost_suffix);
+        return buffer;
+    }
+    return handle->fakehost;
+}
+
+static void
+apply_fakehost(struct handle_info *handle)
+{
+    struct userNode *target;
+    char *fake;
+
+    if (!handle->users)
+        return;
+    fake = generate_fakehost(handle);
+    for (target = handle->users; target; target = target->next_authed)
+        assign_fakehost(target, fake, 1);
+}
+
 static void
 set_user_handle_info(struct userNode *user, struct handle_info *hi, int stamp)
 {
@@ -867,6 +912,9 @@ set_user_handle_info(struct userNode *user, struct handle_info *hi, int stamp)
        if (IsHelper(user))
             userList_append(&curr_helpers, user);
 
+        if (hi->fakehost || old_info)
+            apply_fakehost(hi);
+
         if (stamp) {
 #ifdef WITH_PROTOCOL_BAHAMUT
             /* Stamp users with their account ID. */
@@ -917,6 +965,7 @@ nickserv_register(struct userNode *user, struct userNode *settee, const char *ha
     hi = register_handle(handle, crypted, 0);
     hi->masks = alloc_string_list(1);
     hi->users = NULL;
+    hi->language = lang_C;
     hi->registered = now;
     hi->lastseen = now;
     hi->flags = HI_DEFAULT_FLAGS;
@@ -973,17 +1022,17 @@ nickserv_make_cookie(struct userNode *user, struct handle_info *hi, enum cookie_
     case ACTIVATION:
         hi->passwd[0] = 0; /* invalidate password */
         send_message(user, nickserv, "NSMSG_USE_COOKIE_REGISTER");
-        fmt = user_find_message(user, "NSEMAIL_ACTIVATION_SUBJECT");
+        fmt = handle_find_message(hi, "NSEMAIL_ACTIVATION_SUBJECT");
         snprintf(subject, sizeof(subject), fmt, netname);
-        fmt = user_find_message(user, "NSEMAIL_ACTIVATION_BODY");
+        fmt = handle_find_message(hi, "NSEMAIL_ACTIVATION_BODY");
         snprintf(body, sizeof(body), fmt, netname, cookie->cookie, nickserv->nick, self->name, hi->handle);
         first_time = 1;
         break;
     case PASSWORD_CHANGE:
         send_message(user, nickserv, "NSMSG_USE_COOKIE_RESETPASS");
-        fmt = user_find_message(user, "NSEMAIL_PASSWORD_CHANGE_SUBJECT");
+        fmt = handle_find_message(hi, "NSEMAIL_PASSWORD_CHANGE_SUBJECT");
         snprintf(subject, sizeof(subject), fmt, netname);
-        fmt = user_find_message(user, "NSEMAIL_PASSWORD_CHANGE_BODY");
+        fmt = handle_find_message(hi, "NSEMAIL_PASSWORD_CHANGE_BODY");
         snprintf(body, sizeof(body), fmt, netname, cookie->cookie, nickserv->nick, self->name, hi->handle);
         break;
     case EMAIL_CHANGE:
@@ -991,36 +1040,37 @@ nickserv_make_cookie(struct userNode *user, struct handle_info *hi, enum cookie_
         hi->email_addr = cookie->data;
         if (misc) {
             send_message(user, nickserv, "NSMSG_USE_COOKIE_EMAIL_2");
-            fmt = user_find_message(user, "NSEMAIL_EMAIL_CHANGE_SUBJECT");
+            fmt = handle_find_message(hi, "NSEMAIL_EMAIL_CHANGE_SUBJECT");
             snprintf(subject, sizeof(subject), fmt, netname);
-            fmt = user_find_message(user, "NSEMAIL_EMAIL_CHANGE_BODY_NEW");
+            fmt = handle_find_message(hi, "NSEMAIL_EMAIL_CHANGE_BODY_NEW");
             snprintf(body, sizeof(body), fmt, netname, cookie->cookie+COOKIELEN/2, nickserv->nick, self->name, hi->handle, COOKIELEN/2);
-            sendmail(nickserv, hi, subject, body, 1);
-            fmt = user_find_message(user, "NSEMAIL_EMAIL_CHANGE_BODY_OLD");
-            snprintf(body, sizeof(body), fmt, netname, cookie->cookie+COOKIELEN/2, nickserv->nick, self->name, hi->handle, COOKIELEN/2, hi->email_addr);
+            mail_send(nickserv, hi, subject, body, 1);
+            fmt = handle_find_message(hi, "NSEMAIL_EMAIL_CHANGE_BODY_OLD");
+            snprintf(body, sizeof(body), fmt, netname, cookie->cookie, nickserv->nick, self->name, hi->handle, COOKIELEN/2, hi->email_addr);
         } else {
             send_message(user, nickserv, "NSMSG_USE_COOKIE_EMAIL_1");
-            fmt = user_find_message(user, "NSEMAIL_EMAIL_VERIFY_SUBJECT");
+            fmt = handle_find_message(hi, "NSEMAIL_EMAIL_VERIFY_SUBJECT");
             snprintf(subject, sizeof(subject), fmt, netname);
-            fmt = user_find_message(user, "NSEMAIL_EMAIL_VERIFY_BODY");
+            fmt = handle_find_message(hi, "NSEMAIL_EMAIL_VERIFY_BODY");
             snprintf(body, sizeof(body), fmt, netname, cookie->cookie, nickserv->nick, self->name, hi->handle);
-            sendmail(nickserv, hi, subject, body, 1);
+            mail_send(nickserv, hi, subject, body, 1);
             subject[0] = 0;
         }
         hi->email_addr = misc;
         break;
     case ALLOWAUTH:
-        fmt = user_find_message(user, "NSEMAIL_ALLOWAUTH_SUBJECT");
+        fmt = handle_find_message(hi, "NSEMAIL_ALLOWAUTH_SUBJECT");
         snprintf(subject, sizeof(subject), fmt, netname);
-        fmt = user_find_message(user, "NSEMAIL_ALLOWAUTH_BODY");
+        fmt = handle_find_message(hi, "NSEMAIL_ALLOWAUTH_BODY");
         snprintf(body, sizeof(body), fmt, netname, cookie->cookie, nickserv->nick, self->name, hi->handle);
+        send_message(user, nickserv, "NSMSG_USE_COOKIE_AUTH");
         break;
     default:
         log_module(NS_LOG, LOG_ERROR, "Bad cookie type %d in nickserv_make_cookie.", cookie->type);
         break;
     }
     if (subject[0])
-        sendmail(nickserv, hi, subject, body, first_time);
+        mail_send(nickserv, hi, subject, body, first_time);
     nickserv_bake_cookie(cookie);
 }
 
@@ -1064,6 +1114,7 @@ nickserv_set_email_addr(struct handle_info *hi, const char *new_email_addr)
 
 static NICKSERV_FUNC(cmd_register)
 {
+    irc_in_addr_t ip;
     struct handle_info *hi;
     const char *email_addr, *password;
     int no_auth;
@@ -1079,6 +1130,11 @@ static NICKSERV_FUNC(cmd_register)
         return 0;
     }
 
+    if (IsRegistering(user)) {
+        reply("NSMSG_ALREADY_REGISTERING");
+       return 0;
+    }
+
     if (IsStamped(user)) {
         /* Unauthenticated users might still have been stamped
            previously and could therefore have a hidden host;
@@ -1108,7 +1164,7 @@ static NICKSERV_FUNC(cmd_register)
         }
 
         /* .. and that we are allowed to send to it. */
-        if ((str = sendmail_prohibited_address(email_addr))) {
+        if ((str = mail_prohibited_address(email_addr))) {
             reply("NSMSG_EMAIL_PROHIBITED", email_addr, str);
             return 0;
         }
@@ -1143,7 +1199,7 @@ static NICKSERV_FUNC(cmd_register)
         string_list_append(hi->masks, strdup("*@*"));
     } else {
         string_list_append(hi->masks, generate_hostmask(user, GENMASK_OMITNICK|GENMASK_NO_HIDING|GENMASK_ANY_IDENT));
-        if (user->ip.s_addr && user->hostname[strspn(user->hostname, "0123456789.")])
+        if (irc_in_addr_is_valid(user->ip) && !irc_pton(&ip, NULL, user->hostname))
             string_list_append(hi->masks, generate_hostmask(user, GENMASK_OMITNICK|GENMASK_BYIP|GENMASK_NO_HIDING|GENMASK_ANY_IDENT));
     }
 
@@ -1161,6 +1217,9 @@ static NICKSERV_FUNC(cmd_register)
     if (no_auth)
         nickserv_make_cookie(user, hi, ACTIVATION, hi->passwd);
 
+    /* Set registering flag.. */
+    user->modes |= FLAGS_REGISTERING;
+
     return 1;
 }
 
@@ -1170,14 +1229,17 @@ static NICKSERV_FUNC(cmd_oregister)
     struct userNode *settee;
     struct handle_info *hi;
 
-    NICKSERV_MIN_PARMS(4);
+    NICKSERV_MIN_PARMS(3);
 
     if (!is_valid_handle(argv[1])) {
         reply("NSMSG_BAD_HANDLE", argv[1]);
         return 0;
     }
 
-    if (strchr(argv[3], '@')) {
+    if (argc < 4) {
+        mask = NULL;
+        settee = NULL;
+    } else if (strchr(argv[3], '@')) {
        mask = canonicalize_hostmask(strdup(argv[3]));
        if (argc > 4) {
            settee = GetUserH(argv[4]);
@@ -1204,7 +1266,8 @@ static NICKSERV_FUNC(cmd_oregister)
         free(mask);
         return 0;
     }
-    string_list_append(hi->masks, mask);
+    if (mask)
+        string_list_append(hi->masks, mask);
     return 1;
 }
 
@@ -1233,7 +1296,7 @@ static NICKSERV_FUNC(cmd_handleinfo)
     reply("NSMSG_HANDLEINFO_REGGED", ctime(&hi->registered));
 
     if (!hi->users) {
-       intervalString(buff, now - hi->lastseen);
+       intervalString(buff, now - hi->lastseen, user->handle_info);
        reply("NSMSG_HANDLEINFO_LASTSEEN", buff);
     } else {
        reply("NSMSG_HANDLEINFO_LASTSEEN_NOW");
@@ -1247,7 +1310,7 @@ static NICKSERV_FUNC(cmd_handleinfo)
         struct do_not_register *dnr;
         if ((dnr = chanserv_is_dnr(NULL, hi)))
             reply("NSMSG_HANDLEINFO_DNR", dnr->setter, dnr->reason);
-        if (!oper_outranks(user, hi))
+        if ((user->handle_info->opserv_level < 900) && !oper_outranks(user, hi))
             return 1;
     } else if (hi != user->handle_info)
         return 1;
@@ -1286,6 +1349,9 @@ static NICKSERV_FUNC(cmd_handleinfo)
         reply("NSMSG_HANDLEINFO_EPITHET", (hi->epithet ? hi->epithet : nsmsg_none));
     }
 
+    if (hi->fakehost)
+        reply("NSMSG_HANDLEINFO_FAKEHOST", (hi->fakehost ? hi->fakehost : handle_find_message(hi, "MSG_NONE")));
+
     if (hi->last_quit_host[0])
         reply("NSMSG_HANDLEINFO_LAST_HOST", hi->last_quit_host);
     else
@@ -1507,18 +1573,24 @@ static NICKSERV_FUNC(cmd_auth)
         reply("NSMSG_HANDLE_NOT_FOUND");
         return 0;
     }
+    /* Responses from here on look up the language used by the handle they asked about. */
     passwd = argv[pw_arg];
     if (!valid_user_for(user, hi)) {
         if (hi->email_addr && nickserv_conf.email_enabled)
-            reply("NSMSG_USE_AUTHCOOKIE", hi->handle, hi->handle);
+            send_message_type(4, user, cmd->parent->bot,
+                              handle_find_message(hi, "NSMSG_USE_AUTHCOOKIE"),
+                              hi->handle);
         else
-            reply("NSMSG_HOSTMASK_INVALID", hi->handle);
+            send_message_type(4, user, cmd->parent->bot,
+                              handle_find_message(hi, "NSMSG_HOSTMASK_INVALID"),
+                              hi->handle);
         argv[pw_arg] = "BADMASK";
         return 1;
     }
     if (!checkpass(passwd, hi->passwd)) {
         unsigned int n;
-        reply("NSMSG_PASSWORD_INVALID");
+        send_message_type(4, user, cmd->parent->bot,
+                          handle_find_message(hi, "NSMSG_PASSWORD_INVALID"));
         argv[pw_arg] = "BADPASS";
         for (n=0; n<failpw_func_used; n++) failpw_func_list[n](user, hi);
         if (nickserv_conf.autogag_enabled) {
@@ -1538,29 +1610,37 @@ static NICKSERV_FUNC(cmd_auth)
         return 1;
     }
     if (HANDLE_FLAGGED(hi, SUSPENDED)) {
-        reply("NSMSG_HANDLE_SUSPENDED");
+        send_message_type(4, user, cmd->parent->bot,
+                          handle_find_message(hi, "NSMSG_HANDLE_SUSPENDED"));
         argv[pw_arg] = "SUSPENDED";
         return 1;
     }
     maxlogins = hi->maxlogins ? hi->maxlogins : nickserv_conf.default_maxlogins;
     for (used = 0, other = hi->users; other; other = other->next_authed) {
         if (++used >= maxlogins) {
-            reply("NSMSG_MAX_LOGINS", maxlogins);
+            send_message_type(4, user, cmd->parent->bot,
+                              handle_find_message(hi, "NSMSG_MAX_LOGINS"),
+                              maxlogins);
             argv[pw_arg] = "MAXLOGINS";
             return 1;
         }
     }
 
+    set_user_handle_info(user, hi, 1);
     if (nickserv_conf.email_required && !hi->email_addr)
         reply("NSMSG_PLEASE_SET_EMAIL");
     if (!is_secure_password(hi->handle, passwd, NULL))
         reply("NSMSG_WEAK_PASSWORD");
     if (hi->passwd[0] != '$')
         cryptpass(passwd, hi->passwd);
-
-    reply("NSMSG_AUTH_SUCCESS");
+    if (!hi->masks->used) {
+        irc_in_addr_t ip;
+        string_list_append(hi->masks, generate_hostmask(user, GENMASK_OMITNICK|GENMASK_NO_HIDING|GENMASK_ANY_IDENT));
+        if (irc_in_addr_is_valid(user->ip) && irc_pton(&ip, NULL, user->hostname))
+            string_list_append(hi->masks, generate_hostmask(user, GENMASK_OMITNICK|GENMASK_BYIP|GENMASK_NO_HIDING|GENMASK_ANY_IDENT));
+    }
     argv[pw_arg] = "****";
-    set_user_handle_info(user, hi, 1);
+    reply("NSMSG_AUTH_SUCCESS");
     return 1;
 }
 
@@ -1601,7 +1681,7 @@ static NICKSERV_FUNC(cmd_allowauth)
         /* Unauthenticated users might still have been stamped
            previously and could therefore have a hidden host;
            do not allow them to authenticate to an account. */
-        send_message(target, nickserv, "NSMSG_USER_PREV_STAMP", target->nick);
+        reply("NSMSG_USER_PREV_STAMP", target->nick);
         return 0;
     }
     if (argc == 2)
@@ -1662,7 +1742,6 @@ static NICKSERV_FUNC(cmd_authcookie)
         return 0;
     }
     nickserv_make_cookie(user, hi, ALLOWAUTH, NULL);
-    reply("NSMSG_USE_COOKIE_AUTH");
     return 1;
 }
 
@@ -1774,10 +1853,14 @@ static NICKSERV_FUNC(cmd_cookie)
         nickserv_set_email_addr(hi, hi->cookie->data);
         reply("NSMSG_EMAIL_CHANGED");
         break;
-    case ALLOWAUTH:
+    case ALLOWAUTH: {
+        char *mask = generate_hostmask(user, GENMASK_OMITNICK|GENMASK_NO_HIDING|GENMASK_ANY_IDENT);
         set_user_handle_info(user, hi, 1);
+        nickserv_addmask(user, hi, mask);
         reply("NSMSG_AUTH_SUCCESS");
+        free(mask);
         break;
+    }
     default:
         reply("NSMSG_BAD_COOKIE_TYPE", hi->cookie->type);
         log_module(NS_LOG, LOG_ERROR, "Bad cookie type %d for account %s.", hi->cookie->type, hi->handle);
@@ -1902,13 +1985,13 @@ static NICKSERV_FUNC(cmd_oaddmask)
 }
 
 static int
-nickserv_delmask(struct userNode *user, struct handle_info *hi, const char *del_mask)
+nickserv_delmask(struct userNode *user, struct handle_info *hi, const char *del_mask, int force)
 {
     unsigned int i;
     for (i=0; i<hi->masks->used; i++) {
        if (!strcmp(del_mask, hi->masks->list[i])) {
            char *old_mask = hi->masks->list[i];
-           if (hi->masks->used == 1) {
+           if (hi->masks->used == 1 && !force) {
                send_message(user, nickserv, "NSMSG_DELMASK_NOTLAST");
                return 0;
            }
@@ -1925,7 +2008,7 @@ nickserv_delmask(struct userNode *user, struct handle_info *hi, const char *del_
 static NICKSERV_FUNC(cmd_delmask)
 {
     NICKSERV_MIN_PARMS(2);
-    return nickserv_delmask(user, user->handle_info, argv[1]);
+    return nickserv_delmask(user, user->handle_info, argv[1], 0);
 }
 
 static NICKSERV_FUNC(cmd_odelmask)
@@ -1934,7 +2017,7 @@ static NICKSERV_FUNC(cmd_odelmask)
     NICKSERV_MIN_PARMS(3);
     if (!(hi = get_victim_oper(user, argv[1])))
         return 0;
-    return nickserv_delmask(user, hi, argv[2]);
+    return nickserv_delmask(user, hi, argv[2], 1);
 }
 
 int
@@ -2018,7 +2101,7 @@ set_list(struct userNode *user, struct handle_info *hi, int override)
     unsigned int i;
     char *set_display[] = {
         "INFO", "WIDTH", "TABLEWIDTH", "COLOR", "PRIVMSG", "STYLE",
-        "EMAIL", "ANNOUNCEMENTS", "MAXLOGINS"
+        "EMAIL", "ANNOUNCEMENTS", "MAXLOGINS", "LANGUAGE"
     };
 
     send_message(user, nickserv, "NSMSG_SETTING_LIST");
@@ -2088,10 +2171,8 @@ static OPTION_FUNC(opt_info)
 
 static OPTION_FUNC(opt_width)
 {
-    if (argc > 1) {
-       unsigned int new_width = strtoul(argv[1], NULL, 0);
-       hi->screen_width = new_width;
-    }
+    if (argc > 1)
+       hi->screen_width = strtoul(argv[1], NULL, 0);
 
     if ((hi->screen_width > 0) && (hi->screen_width < MIN_LINE_SIZE))
         hi->screen_width = MIN_LINE_SIZE;
@@ -2104,10 +2185,8 @@ static OPTION_FUNC(opt_width)
 
 static OPTION_FUNC(opt_tablewidth)
 {
-    if (argc > 1) {
-       unsigned int new_width = strtoul(argv[1], NULL, 0);
-       hi->table_width = new_width;
-    }
+    if (argc > 1)
+       hi->table_width = strtoul(argv[1], NULL, 0);
 
     if ((hi->table_width > 0) && (hi->table_width < MIN_LINE_SIZE))
         hi->table_width = MIN_LINE_SIZE;
@@ -2249,7 +2328,7 @@ static OPTION_FUNC(opt_email)
             send_message(user, nickserv, "NSMSG_BAD_EMAIL_ADDR");
             return 0;
         }
-        if ((str = sendmail_prohibited_address(argv[1]))) {
+        if ((str = mail_prohibited_address(argv[1]))) {
             send_message(user, nickserv, "NSMSG_EMAIL_PROHIBITED", argv[1], str);
             return 0;
         }
@@ -2270,7 +2349,7 @@ static OPTION_FUNC(opt_email)
 
 static OPTION_FUNC(opt_maxlogins)
 {
-    char maxlogins;
+    unsigned char maxlogins;
     if (argc > 1) {
         maxlogins = strtoul(argv[1], NULL, 0);
         if ((maxlogins > nickserv_conf.hard_maxlogins) && !override) {
@@ -2293,8 +2372,7 @@ static OPTION_FUNC(opt_language)
             send_message(user, nickserv, "NSMSG_LANGUAGE_NOT_FOUND", argv[1], lang->name);
         hi->language = lang;
     }
-    lang = hi->language ? hi->language : lang_C;
-    send_message(user, nickserv, "NSMSG_SET_LANGUAGE", lang->name);
+    send_message(user, nickserv, "NSMSG_SET_LANGUAGE", hi->language->name);
     return 1;
 }
 
@@ -2364,6 +2442,76 @@ static OPTION_FUNC(opt_epithet)
     return 1;
 }
 
+static OPTION_FUNC(opt_title)
+{
+    const char *title;
+
+    if (!override) {
+        send_message(user, nickserv, "MSG_SETTING_PRIVILEGED", argv[0]);
+        return 0;
+    }
+
+    if ((argc > 1) && oper_has_access(user, nickserv, nickserv_conf.set_title_level, 0)) {
+        title = argv[1];
+        if (strchr(title, '.')) {
+            send_message(user, nickserv, "NSMSG_TITLE_INVALID");
+            return 0;
+        }
+        if ((strlen(user->handle_info->handle) + strlen(title) +
+             strlen(nickserv_conf.titlehost_suffix) + 2) > HOSTLEN) {
+            send_message(user, nickserv, "NSMSG_TITLE_TRUNCATED");
+            return 0;
+        }
+
+        free(hi->fakehost);
+        if (!strcmp(title, "*")) {
+            hi->fakehost = NULL;
+        } else {
+            hi->fakehost = malloc(strlen(title)+2);
+            hi->fakehost[0] = '.';
+            strcpy(hi->fakehost+1, title);
+        }
+        apply_fakehost(hi);
+    } else if (hi->fakehost && (hi->fakehost[0] == '.'))
+        title = hi->fakehost + 1;
+    else
+        title = NULL;
+    if (!title)
+        title = user_find_message(user, "MSG_NONE");
+    send_message(user, nickserv, "NSMSG_SET_TITLE", title);
+    return 1;
+}
+
+static OPTION_FUNC(opt_fakehost)
+{
+    const char *fake;
+
+    if (!override) {
+        send_message(user, nickserv, "MSG_SETTING_PRIVILEGED", argv[0]);
+        return 0;
+    }
+
+    if ((argc > 1) && oper_has_access(user, nickserv, nickserv_conf.set_fakehost_level, 0)) {
+        fake = argv[1];
+        if ((strlen(fake) > HOSTLEN) || (fake[0] == '.')) {
+            send_message(user, nickserv, "NSMSG_FAKEHOST_INVALID", HOSTLEN);
+            return 0;
+        }
+        free(hi->fakehost);
+        if (!strcmp(fake, "*"))
+            hi->fakehost = NULL;
+        else
+            hi->fakehost = strdup(fake);
+        fake = hi->fakehost;
+        apply_fakehost(hi);
+    } else
+        fake = generate_fakehost(hi);
+    if (!fake)
+        fake = user_find_message(user, "MSG_NONE");
+    send_message(user, nickserv, "NSMSG_SET_FAKEHOST", fake);
+    return 1;
+}
+
 static NICKSERV_FUNC(cmd_reclaim)
 {
     struct handle_info *hi;
@@ -2431,10 +2579,8 @@ static NICKSERV_FUNC(cmd_ounregnick)
        reply("NSMSG_NICK_NOT_REGISTERED", argv[1]);
        return 0;
     }
-    if (ni->owner->opserv_level >= user->handle_info->opserv_level) {
-       reply("MSG_USER_OUTRANKED", ni->nick);
-       return 0;
-    }
+    if (!oper_outranks(user, ni->owner))
+        return 0;
     reply("NSMSG_UNREGNICK_SUCCESS", ni->nick);
     delete_nick(ni);
     return 1;
@@ -2462,12 +2608,15 @@ static NICKSERV_FUNC(cmd_unregister)
 static NICKSERV_FUNC(cmd_ounregister)
 {
     struct handle_info *hi;
+    char reason[MAXLEN];
 
     NICKSERV_MIN_PARMS(2);
     if (!(hi = get_victim_oper(user, argv[1])))
         return 0;
+    snprintf(reason, sizeof(reason), "%s unregistered account %s.", user->handle_info->handle, hi->handle);
+    global_message(MESSAGE_RECIPIENT_STAFF, reason);
     nickserv_unregister_handle(hi, user);
-    return 0;
+    return 1;
 }
 
 static NICKSERV_FUNC(cmd_status)
@@ -2564,6 +2713,8 @@ nickserv_saxdb_write(struct saxdb_context *ctx) {
             saxdb_write_string(ctx, KEY_EMAIL_ADDR, hi->email_addr);
         if (hi->epithet)
             saxdb_write_string(ctx, KEY_EPITHET, hi->epithet);
+        if (hi->fakehost)
+            saxdb_write_string(ctx, KEY_FAKEHOST, hi->fakehost);
         if (hi->flags) {
             int ii, flen;
 
@@ -2597,7 +2748,7 @@ nickserv_saxdb_write(struct saxdb_context *ctx) {
         }
         if (hi->opserv_level)
             saxdb_write_int(ctx, KEY_OPSERV_LEVEL, hi->opserv_level);
-        if (hi->language && (hi->language != lang_C))
+        if (hi->language != lang_C)
             saxdb_write_string(ctx, KEY_LANGUAGE, hi->language->name);
         saxdb_write_string(ctx, KEY_PASSWD, hi->passwd);
         saxdb_write_int(ctx, KEY_REGISTER_ON, hi->registered);
@@ -2694,19 +2845,17 @@ static NICKSERV_FUNC(cmd_merge)
         for (cList2=hi_to->channels; cList2; cList2=cList2->u_next)
             if (cList->channel == cList2->channel)
                 break;
-        log_module(NS_LOG, LOG_DEBUG, "Merging %s->%s@%s: before %p->%p->%-p, %p->%p->%p",
-                   hi_from->handle, hi_to->handle, cList->channel->channel->name,
-                   cList->u_prev, cList, cList->u_next,
-                   (cList2?cList2->u_prev:0), cList2, (cList2?cList2->u_next:0));
         if (cList2 && (cList2->access >= cList->access)) {
+            log_module(NS_LOG, LOG_INFO, "Merge: %s had only %d access in %s (versus %d for %s)", hi_from->handle, cList->access, cList->channel->channel->name, cList2->access, hi_to->handle);
             /* keep cList2 in hi_to; remove cList from hi_from */
-            log_module(NS_LOG, LOG_DEBUG, "Deleting %p", cList);
             del_channel_user(cList, 1);
         } else {
             if (cList2) {
+                log_module(NS_LOG, LOG_INFO, "Merge: %s had only %d access in %s (versus %d for %s)", hi_to->handle, cList2->access, cList->channel->channel->name, cList->access, hi_from->handle);
                 /* remove the lower-ranking cList2 from hi_to */
-                log_module(NS_LOG, LOG_DEBUG, "Deleting %p", cList2);
                 del_channel_user(cList2, 1);
+            } else {
+                log_module(NS_LOG, LOG_INFO, "Merge: %s had no access in %s", hi_to->handle, cList->channel->channel->name);
             }
             /* cList needs to be moved from hi_from to hi_to */
             cList->handle = hi_to;
@@ -2721,8 +2870,6 @@ static NICKSERV_FUNC(cmd_merge)
             if (hi_to->channels)
                 hi_to->channels->u_prev = cList;
             hi_to->channels = cList;
-            log_module(NS_LOG, LOG_DEBUG, "Now %p->%p->%p",
-                       cList->u_prev, cList, cList->u_next);
         }
     }
 
@@ -2734,6 +2881,13 @@ static NICKSERV_FUNC(cmd_merge)
     if (hi_from->lastseen > hi_to->lastseen)
         hi_to->lastseen = hi_from->lastseen;
 
+    /* Does a fakehost carry over?  (This intentionally doesn't set it
+     * for users previously attached to hi_to.  They'll just have to
+     * reconnect.)
+     */
+    if (hi_from->fakehost && !hi_to->fakehost)
+        hi_to->fakehost = strdup(hi_from->fakehost);
+
     /* Notify of success. */
     sprintf(buffer, "%s (%s) merged account %s into %s.", user->nick, user->handle_info->handle, hi_from->handle, hi_to->handle);
     reply("NSMSG_HANDLES_MERGED", hi_from->handle, hi_to->handle);
@@ -2787,7 +2941,7 @@ nickserv_discrim_create(struct userNode *user, unsigned int argc, char *argv[])
             goto fail;
         }
         if (!irccasecmp(argv[i], "limit")) {
-            discrim->limit = atoi(argv[++i]);
+            discrim->limit = strtoul(argv[++i], NULL, 0);
         } else if (!irccasecmp(argv[i], "flags")) {
             nickserv_modify_handle_flags(user, nickserv, argv[++i], &discrim->flags_on, &discrim->flags_off);
         } else if (!irccasecmp(argv[i], "registered")) {
@@ -3007,8 +3161,8 @@ nickserv_show_oper_accounts(struct userNode *user, struct svccmd *cmd)
     }
     table_send(cmd->parent->bot, user->nick, 0, NULL, tbl);
     reply("MSG_MATCH_COUNT", hil.used);
-    for (ii = 0; ii < hil.used; )
-        free(tbl.contents[++ii]);
+    for (ii = 0; ii < hil.used; ii++)
+        free(tbl.contents[ii]);
     free(tbl.contents);
     free(hil.list);
 }
@@ -3082,6 +3236,7 @@ nickserv_db_read_handle(const char *handle, dict_t obj)
     struct string_list *masks, *slist;
     struct handle_info *hi;
     struct userNode *authed_users;
+    struct userData *channels;
     unsigned long int id;
     unsigned int ii;
     dict_t subdb;
@@ -3095,10 +3250,13 @@ nickserv_db_read_handle(const char *handle, dict_t obj)
     }
     if ((hi = get_handle_info(handle))) {
         authed_users = hi->users;
+        channels = hi->channels;
         hi->users = NULL;
+        hi->channels = NULL;
         dict_remove(nickserv_handle_dict, hi->handle);
     } else {
         authed_users = NULL;
+        channels = NULL;
     }
     hi = register_handle(handle, str, id);
     if (authed_users) {
@@ -3108,6 +3266,7 @@ nickserv_db_read_handle(const char *handle, dict_t obj)
             authed_users = authed_users->next_authed;
         }
     }
+    hi->channels = channels;
     masks = database_get_data(obj, KEY_MASKS, RECDB_STRING_LIST);
     hi->masks = masks ? string_list_copy(masks) : alloc_string_list(1);
     str = database_get_data(obj, KEY_MAXLOGINS, RECDB_QSTRING);
@@ -3117,7 +3276,8 @@ nickserv_db_read_handle(const char *handle, dict_t obj)
     str = database_get_data(obj, KEY_OPSERV_LEVEL, RECDB_QSTRING);
     hi->opserv_level = str ? strtoul(str, NULL, 0) : 0;
     str = database_get_data(obj, KEY_INFO, RECDB_QSTRING);
-    if (str) hi->infoline = strdup(str);
+    if (str)
+        hi->infoline = strdup(str);
     str = database_get_data(obj, KEY_REGISTER_ON, RECDB_QSTRING);
     hi->registered = str ? (time_t)strtoul(str, NULL, 0) : now;
     str = database_get_data(obj, KEY_LAST_SEEN, RECDB_QSTRING);
@@ -3143,12 +3303,19 @@ nickserv_db_read_handle(const char *handle, dict_t obj)
     str = database_get_data(obj, KEY_TABLE_WIDTH, RECDB_QSTRING);
     hi->table_width = str ? strtoul(str, NULL, 0) : 0;
     str = database_get_data(obj, KEY_LAST_QUIT_HOST, RECDB_QSTRING);
-    if (!str) str = database_get_data(obj, KEY_LAST_AUTHED_HOST, RECDB_QSTRING);
-    if (str) safestrncpy(hi->last_quit_host, str, sizeof(hi->last_quit_host));
+    if (!str)
+        str = database_get_data(obj, KEY_LAST_AUTHED_HOST, RECDB_QSTRING);
+    if (str)
+        safestrncpy(hi->last_quit_host, str, sizeof(hi->last_quit_host));
     str = database_get_data(obj, KEY_EMAIL_ADDR, RECDB_QSTRING);
-    if (str) nickserv_set_email_addr(hi, str);
+    if (str)
+        nickserv_set_email_addr(hi, str);
     str = database_get_data(obj, KEY_EPITHET, RECDB_QSTRING);
-    if (str) hi->epithet = strdup(str);
+    if (str)
+        hi->epithet = strdup(str);
+    str = database_get_data(obj, KEY_FAKEHOST, RECDB_QSTRING);
+    if (str)
+        hi->fakehost = strdup(str);
     subdb = database_get_data(obj, KEY_COOKIE, RECDB_OBJECT);
     if (subdb) {
         const char *data, *type, *expires, *cookie_str;
@@ -3300,8 +3467,10 @@ nickserv_conf_read(void)
        return;
     }
     str = database_get_data(conf_node, KEY_VALID_HANDLE_REGEX, RECDB_QSTRING);
-    if (!str) str = database_get_data(conf_node, KEY_VALID_ACCOUNT_REGEX, RECDB_QSTRING);
-    if (nickserv_conf.valid_handle_regex_set) regfree(&nickserv_conf.valid_handle_regex);
+    if (!str)
+        str = database_get_data(conf_node, KEY_VALID_ACCOUNT_REGEX, RECDB_QSTRING);
+    if (nickserv_conf.valid_handle_regex_set)
+        regfree(&nickserv_conf.valid_handle_regex);
     if (str) {
         int err = regcomp(&nickserv_conf.valid_handle_regex, str, REG_EXTENDED|REG_ICASE|REG_NOSUB);
         nickserv_conf.valid_handle_regex_set = !err;
@@ -3310,7 +3479,8 @@ nickserv_conf_read(void)
         nickserv_conf.valid_handle_regex_set = 0;
     }
     str = database_get_data(conf_node, KEY_VALID_NICK_REGEX, RECDB_QSTRING);
-    if (nickserv_conf.valid_nick_regex_set) regfree(&nickserv_conf.valid_nick_regex);
+    if (nickserv_conf.valid_nick_regex_set)
+        regfree(&nickserv_conf.valid_nick_regex);
     if (str) {
         int err = regcomp(&nickserv_conf.valid_nick_regex, str, REG_EXTENDED|REG_ICASE|REG_NOSUB);
         nickserv_conf.valid_nick_regex_set = !err;
@@ -3319,7 +3489,8 @@ nickserv_conf_read(void)
         nickserv_conf.valid_nick_regex_set = 0;
     }
     str = database_get_data(conf_node, KEY_NICKS_PER_HANDLE, RECDB_QSTRING);
-    if (!str) str = database_get_data(conf_node, KEY_NICKS_PER_ACCOUNT, RECDB_QSTRING);
+    if (!str)
+        str = database_get_data(conf_node, KEY_NICKS_PER_ACCOUNT, RECDB_QSTRING);
     nickserv_conf.nicks_per_handle = str ? strtoul(str, NULL, 0) : 4;
     str = database_get_data(conf_node, KEY_DISABLE_NICKS, RECDB_QSTRING);
     nickserv_conf.disable_nicks = str ? strtoul(str, NULL, 0) : 0;
@@ -3339,14 +3510,21 @@ nickserv_conf_read(void)
     nickserv_conf.modoper_level = str ? strtoul(str, NULL, 0) : 900;
     str = database_get_data(conf_node, KEY_SET_EPITHET_LEVEL, RECDB_QSTRING);
     nickserv_conf.set_epithet_level = str ? strtoul(str, NULL, 0) : 1;
+    str = database_get_data(conf_node, KEY_SET_TITLE_LEVEL, RECDB_QSTRING);
+    nickserv_conf.set_title_level = str ? strtoul(str, NULL, 0) : 900;
+    str = database_get_data(conf_node, KEY_SET_FAKEHOST_LEVEL, RECDB_QSTRING);
+    nickserv_conf.set_fakehost_level = str ? strtoul(str, NULL, 0) : 1000;
     str = database_get_data(conf_node, KEY_HANDLE_EXPIRE_FREQ, RECDB_QSTRING);
-    if (!str) str = database_get_data(conf_node, KEY_ACCOUNT_EXPIRE_FREQ, RECDB_QSTRING);
+    if (!str)
+        str = database_get_data(conf_node, KEY_ACCOUNT_EXPIRE_FREQ, RECDB_QSTRING);
     nickserv_conf.handle_expire_frequency = str ? ParseInterval(str) : 86400;
     str = database_get_data(conf_node, KEY_HANDLE_EXPIRE_DELAY, RECDB_QSTRING);
-    if (!str) str = database_get_data(conf_node, KEY_ACCOUNT_EXPIRE_DELAY, RECDB_QSTRING);
+    if (!str)
+        str = database_get_data(conf_node, KEY_ACCOUNT_EXPIRE_DELAY, RECDB_QSTRING);
     nickserv_conf.handle_expire_delay = str ? ParseInterval(str) : 86400*30;
     str = database_get_data(conf_node, KEY_NOCHAN_HANDLE_EXPIRE_DELAY, RECDB_QSTRING);
-    if (!str) str = database_get_data(conf_node, KEY_NOCHAN_ACCOUNT_EXPIRE_DELAY, RECDB_QSTRING);
+    if (!str)
+        str = database_get_data(conf_node, KEY_NOCHAN_ACCOUNT_EXPIRE_DELAY, RECDB_QSTRING);
     nickserv_conf.nochan_handle_expire_delay = str ? ParseInterval(str) : 86400*15;
     str = database_get_data(conf_node, "warn_clone_auth", RECDB_QSTRING);
     nickserv_conf.warn_clone_auth = str ? !disabled_string(str) : 1;
@@ -3410,6 +3588,8 @@ nickserv_conf_read(void)
     nickserv_conf.handles_per_email = str ? strtoul(str, NULL, 0) : 1;
     str = database_get_data(conf_node, KEY_EMAIL_SEARCH_LEVEL, RECDB_QSTRING);
     nickserv_conf.email_search_level = str ? strtoul(str, NULL, 0) : 600;
+    str = database_get_data(conf_node, KEY_TITLEHOST_SUFFIX, RECDB_QSTRING);
+    nickserv_conf.titlehost_suffix = str ? str : "example.net";
     str = conf_get_data("server/network", RECDB_QSTRING);
     nickserv_conf.network_name = str ? str : "some IRC network";
     if (!nickserv_conf.auth_policer_params) {
@@ -3424,6 +3604,7 @@ nickserv_conf_read(void)
 
 static void
 nickserv_reclaim(struct userNode *user, struct nick_info *ni, enum reclaim_action action) {
+    const char *msg;
     char newnick[NICKLEN+1];
 
     assert(user);
@@ -3442,7 +3623,8 @@ nickserv_reclaim(struct userNode *user, struct nick_info *ni, enum reclaim_actio
         irc_svsnick(nickserv, user, newnick);
         break;
     case RECLAIM_KILL:
-        irc_kill(nickserv, user, "NSMSG_RECLAIM_KILL");
+        msg = user_find_message(user, "NSMSG_RECLAIM_KILL");
+        DelUser(user, nickserv, 1, msg);
         break;
     }
 }
@@ -3617,7 +3799,7 @@ init_nickserv(const char *nick)
     nickserv_define_func("USERINFO", cmd_userinfo, -1, 1, 0);
     nickserv_define_func("RENAME", cmd_rename_handle, -1, 1, 0);
     nickserv_define_func("VACATION", cmd_vacation, -1, 1, 0);
-    nickserv_define_func("MERGE", cmd_merge, 0, 1, 0);
+    nickserv_define_func("MERGE", cmd_merge, 750, 1, 0);
     if (!nickserv_conf.disable_nicks) {
        /* nick management commands */
        nickserv_define_func("REGNICK", cmd_regnick, -1, 1, 0);
@@ -3654,6 +3836,10 @@ init_nickserv(const char *nick)
     dict_insert(nickserv_opt_dict, "ACCESS", opt_level);
     dict_insert(nickserv_opt_dict, "LEVEL", opt_level);
     dict_insert(nickserv_opt_dict, "EPITHET", opt_epithet);
+    if (nickserv_conf.titlehost_suffix) {
+        dict_insert(nickserv_opt_dict, "TITLE", opt_title);
+        dict_insert(nickserv_opt_dict, "FAKEHOST", opt_fakehost);
+    }
     dict_insert(nickserv_opt_dict, "ANNOUNCEMENTS", opt_announcements);
     dict_insert(nickserv_opt_dict, "MAXLOGINS", opt_maxlogins);
     dict_insert(nickserv_opt_dict, "LANGUAGE", opt_language);
@@ -3666,15 +3852,16 @@ init_nickserv(const char *nick)
     dict_set_free_keys(nickserv_id_dict, free);
 
     nickserv_nick_dict = dict_new();
-    dict_set_free_data(nickserv_nick_dict, free_nick_info);
+    dict_set_free_data(nickserv_nick_dict, free);
 
     nickserv_allow_auth_dict = dict_new();
 
     userList_init(&curr_helpers);
 
     if (nick) {
-        nickserv = AddService(nick, "Nick Services");
-        nickserv_service = service_register(nickserv, 0);
+        const char *modes = conf_get_data("services/nickserv/modes", RECDB_QSTRING);
+        nickserv = AddLocalUser(nick, nick, NULL, "Nick Services", modes);
+        nickserv_service = service_register(nickserv);
     }
     saxdb_register("NickServ", nickserv_saxdb_read, nickserv_saxdb_write);
     reg_exit_func(nickserv_db_cleanup);