feat(mailserver): Add mail autoconfig and user repair features

Autoconfig:
- Created config-v1.1.xml (Thunderbird), autodiscover.xml (Outlook),
  email.mobileconfig (Apple) for automatic mail client configuration
- Added uhttpd instance on port 8025 to serve autoconfig files
- Added HAProxy backends with waf_bypass for autoconfig domains
- Added mailctl autoconfig-setup and autoconfig-status commands

LuCI Mailserver:
- Added user_repair method for mailbox repair (doveadm force-resync)
- Added repair button to user actions in overview

LuCI Nextcloud:
- Added list_users method to list Nextcloud users
- Added reset_password method for password reset via OCC

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-02-16 13:26:23 +01:00
parent 856a167ad4
commit 7151bc6138
7 changed files with 425 additions and 6 deletions

View File

@ -2035,3 +2035,31 @@ git checkout HEAD -- index.html
- `secubox-app-mailserver/files/usr/lib/mailserver/users.sh`
- `secubox-app-mailserver/files/usr/sbin/mailctl`
- `luci-app-mailserver/root/usr/libexec/rpcd/luci.mailserver`
### 2026-02-16: Mail Autoconfig & Repair Features
**Mail Autoconfig Setup**
- Created autoconfig files for automatic mail client configuration:
- `config-v1.1.xml` - Mozilla Thunderbird format
- `autodiscover.xml` - Microsoft Outlook format
- `email.mobileconfig` - Apple iOS/macOS format
- Set up uhttpd instance on port 8025 to serve autoconfig files
- Added HAProxy backends with waf_bypass for autoconfig.secubox.in and autoconfig.gk2.secubox.in
- Created mailctl autoconfig-setup and autoconfig-status commands
**LuCI Enhancement: luci-app-mailserver**
- Added `user_repair` method for mailbox repair (doveadm force-resync)
- Added repair button (🔧) to user actions in overview
- Updated ACL with new permission
**LuCI Enhancement: luci-app-nextcloud**
- Added `list_users` method to list Nextcloud users
- Added `reset_password` method for password reset via OCC
- Updated ACL with new permissions
**Files Modified:**
- `luci-app-mailserver/root/usr/libexec/rpcd/luci.mailserver`
- `luci-app-mailserver/htdocs/luci-static/resources/view/mailserver/overview.js`
- `luci-app-mailserver/root/usr/share/rpcd/acl.d/luci-app-mailserver.json`
- `luci-app-nextcloud/root/usr/libexec/rpcd/luci.nextcloud`
- `luci-app-nextcloud/root/usr/share/rpcd/acl.d/luci-app-nextcloud.json`

View File

@ -105,6 +105,13 @@ var callFixPorts = rpc.declare({
expect: {}
});
var callUserRepair = rpc.declare({
object: 'luci.mailserver',
method: 'user_repair',
params: ['email'],
expect: {}
});
return view.extend({
load: function() {
return Promise.all([
@ -333,7 +340,7 @@ return view.extend({
E('th', {}, 'Email'),
E('th', {}, 'Size'),
E('th', {}, 'Msgs'),
E('th', { 'style': 'width: 120px;' }, 'Actions')
E('th', { 'style': 'width: 160px;' }, 'Actions')
])
]),
E('tbody', {}, users.map(function(u) {
@ -345,11 +352,19 @@ return view.extend({
E('button', {
'class': 'kiss-btn',
'style': 'padding: 4px 8px; font-size: 11px; margin-right: 4px;',
'title': 'Reset Password',
'click': ui.createHandlerFn(self, self.showResetPasswordModal, u.email)
}, '\ud83d\udd11'),
E('button', {
'class': 'kiss-btn',
'style': 'padding: 4px 8px; font-size: 11px; margin-right: 4px;',
'title': 'Repair Mailbox',
'click': ui.createHandlerFn(self, self.doRepairMailbox, u.email)
}, '\ud83d\udd27'),
E('button', {
'class': 'kiss-btn kiss-btn-red',
'style': 'padding: 4px 8px; font-size: 11px;',
'title': 'Delete User',
'click': ui.createHandlerFn(self, self.doDeleteUser, u.email)
}, '\ud83d\uddd1')
])
@ -598,6 +613,21 @@ return view.extend({
});
},
doRepairMailbox: function(email) {
ui.showModal('Repairing Mailbox', [
E('p', { 'class': 'spinning' }, 'Repairing mailbox for ' + email + '...')
]);
return callUserRepair(email).then(function(res) {
ui.hideModal();
if (res.code === 0) {
ui.addNotification(null, E('p', 'Mailbox repaired for ' + email), 'success');
} else {
ui.addNotification(null, E('p', 'Repair output: ' + (res.output || 'No issues found')), 'info');
}
window.location.reload();
});
},
doDnsSetup: function() {
ui.showModal('DNS Setup', [
E('p', { 'class': 'spinning' }, 'Creating MX, SPF, DKIM, DMARC records...')

View File

@ -29,7 +29,8 @@ case "$1" in
"webmail_configure": {},
"mesh_backup": {},
"mesh_sync": { "mode": "string" },
"fix_ports": {}
"fix_ports": {},
"user_repair": { "email": "string" }
}
EOF
;;
@ -371,6 +372,31 @@ case "$1" in
json_add_string "output" "$output"
json_dump
;;
user_repair)
# Read JSON from stdin or $3
if [ -n "$3" ]; then
json_load "$3"
else
read -r _input
json_load "$_input"
fi
json_get_var email email
json_init
if [ -z "$email" ]; then
json_add_int "code" 1
json_add_string "error" "Email required"
else
container=$(uci -q get $CONFIG.main.container)
container="${container:-mailserver}"
# Run doveadm force-resync to repair mailbox
output=$(lxc-attach -n "$container" -- doveadm force-resync -u "$email" '*' 2>&1)
json_add_int "code" "$?"
json_add_string "output" "$output"
fi
json_dump
;;
esac
;;
esac

View File

@ -9,7 +9,7 @@
},
"write": {
"ubus": {
"luci.mailserver": ["install", "start", "stop", "restart", "user_add", "user_del", "user_passwd", "alias_add", "dns_setup", "ssl_setup", "webmail_configure", "mesh_backup", "mesh_sync", "fix_ports", "alias_del"]
"luci.mailserver": ["install", "start", "stop", "restart", "user_add", "user_del", "user_passwd", "alias_add", "dns_setup", "ssl_setup", "webmail_configure", "mesh_backup", "mesh_sync", "fix_ports", "alias_del", "user_repair"]
},
"uci": ["mailserver"]
}

View File

@ -312,6 +312,61 @@ get_logs() {
echo "{\"logs\": \"$log_content\"}"
}
# List users
list_users() {
if ! lxc_running; then
echo '{"users": []}'
return
fi
local users_json
users_json=$(lxc-attach -n "$LXC_NAME" -- su -s /bin/bash www-data -c "php /var/www/nextcloud/occ user:list --output=json" 2>/dev/null)
if [ -z "$users_json" ] || [ "$users_json" = "{}" ]; then
echo '{"users": []}'
return
fi
# Convert from {uid: displayname} to [{uid: x, displayname: y}]
local users_array="[]"
users_array=$(echo "$users_json" | jsonfilter -e '@' 2>/dev/null | \
awk -F: '{gsub(/[{}"]/,"",$1); gsub(/[{}"]/,"",$2); if($1!="") printf "{\"uid\":\"%s\",\"displayname\":\"%s\"},", $1, $2}' | \
sed 's/,$//' | sed 's/^/[/;s/$/]/')
[ -z "$users_array" ] && users_array="[]"
echo "{\"users\": $users_array}"
}
# Reset user password
reset_password() {
local input
read -r input
local uid=$(echo "$input" | jsonfilter -e '@.uid' 2>/dev/null)
local password=$(echo "$input" | jsonfilter -e '@.password' 2>/dev/null)
if [ -z "$uid" ] || [ -z "$password" ]; then
echo '{"success": false, "error": "User ID and password required"}'
return
fi
if ! lxc_running; then
echo '{"success": false, "error": "Container not running"}'
return
fi
# Reset password via OCC
local result
result=$(lxc-attach -n "$LXC_NAME" -- su -s /bin/bash www-data -c "OC_PASS='$password' php /var/www/nextcloud/occ user:resetpassword --password-from-env '$uid'" 2>&1)
local rc=$?
if [ $rc -eq 0 ]; then
echo '{"success": true, "message": "Password reset for '"$uid"'"}'
else
local escaped=$(echo "$result" | sed 's/"/\\"/g' | tr '\n' ' ')
echo "{\"success\": false, \"error\": \"$escaped\"}"
fi
}
# RPCD list method
list_methods() {
cat <<'EOF'
@ -330,7 +385,9 @@ list_methods() {
"ssl_enable": {"domain": "string"},
"ssl_disable": {},
"occ": {"command": "string"},
"logs": {}
"logs": {},
"list_users": {},
"reset_password": {"uid": "string", "password": "string"}
}
EOF
}
@ -357,6 +414,8 @@ case "$1" in
ssl_disable) do_ssl_disable ;;
occ) do_occ ;;
logs) get_logs ;;
list_users) list_users ;;
reset_password) reset_password ;;
*) echo '{"error": "Unknown method"}' ;;
esac
;;

View File

@ -3,7 +3,7 @@
"description": "Grant access to Nextcloud LXC app",
"read": {
"ubus": {
"luci.nextcloud": ["status", "get_config", "list_backups", "logs"]
"luci.nextcloud": ["status", "get_config", "list_backups", "logs", "list_users"]
},
"uci": ["nextcloud"]
},
@ -20,7 +20,8 @@
"restore",
"ssl_enable",
"ssl_disable",
"occ"
"occ",
"reset_password"
]
},
"uci": ["nextcloud"]

View File

@ -588,6 +588,277 @@ EOF
esac
}
# ============================================================================
# Autoconfig / Autodiscover Setup
# ============================================================================
cmd_autoconfig_setup() {
local domain=$(uci_get main.domain)
local hostname=$(uci_get main.hostname)
hostname="${hostname:-mail}"
local mail_fqdn="${hostname}.${domain}"
local data_path=$(uci_get main.data_path)
data_path="${data_path:-/srv/mailserver}"
if [ -z "$domain" ]; then
error "Domain not configured. Set with: uci set mailserver.main.domain=example.com"
return 1
fi
log "Setting up mail autoconfig for $domain..."
local autoconfig_dir="$data_path/autoconfig"
mkdir -p "$autoconfig_dir/mail"
mkdir -p "$autoconfig_dir/autodiscover"
# Thunderbird autoconfig (config-v1.1.xml)
cat > "$autoconfig_dir/mail/config-v1.1.xml" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<clientConfig version="1.1">
<emailProvider id="$domain">
<domain>$domain</domain>
<displayName>$domain Mail</displayName>
<displayShortName>$domain</displayShortName>
<incomingServer type="imap">
<hostname>$mail_fqdn</hostname>
<port>993</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<incomingServer type="imap">
<hostname>$mail_fqdn</hostname>
<port>143</port>
<socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</incomingServer>
<outgoingServer type="smtp">
<hostname>$mail_fqdn</hostname>
<port>587</port>
<socketType>STARTTLS</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</outgoingServer>
<outgoingServer type="smtp">
<hostname>$mail_fqdn</hostname>
<port>465</port>
<socketType>SSL</socketType>
<authentication>password-cleartext</authentication>
<username>%EMAILADDRESS%</username>
</outgoingServer>
</emailProvider>
</clientConfig>
EOF
# Outlook/ActiveSync autodiscover (autodiscover.xml)
cat > "$autoconfig_dir/autodiscover/autodiscover.xml" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
<Account>
<AccountType>email</AccountType>
<Action>settings</Action>
<Protocol>
<Type>IMAP</Type>
<Server>$mail_fqdn</Server>
<Port>993</Port>
<DomainRequired>off</DomainRequired>
<LoginName></LoginName>
<SPA>off</SPA>
<SSL>on</SSL>
<AuthRequired>on</AuthRequired>
</Protocol>
<Protocol>
<Type>SMTP</Type>
<Server>$mail_fqdn</Server>
<Port>587</Port>
<DomainRequired>off</DomainRequired>
<LoginName></LoginName>
<SPA>off</SPA>
<Encryption>TLS</Encryption>
<AuthRequired>on</AuthRequired>
<UsePOPAuth>on</UsePOPAuth>
<SMTPLast>off</SMTPLast>
</Protocol>
</Account>
</Response>
</Autodiscover>
EOF
# Apple mobileconfig profile
cat > "$autoconfig_dir/$domain.mobileconfig" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>EmailAccountDescription</key>
<string>$domain Mail</string>
<key>EmailAccountName</key>
<string>$domain</string>
<key>EmailAccountType</key>
<string>EmailTypeIMAP</string>
<key>EmailAddress</key>
<string></string>
<key>IncomingMailServerAuthentication</key>
<string>EmailAuthPassword</string>
<key>IncomingMailServerHostName</key>
<string>$mail_fqdn</string>
<key>IncomingMailServerPortNumber</key>
<integer>993</integer>
<key>IncomingMailServerUseSSL</key>
<true/>
<key>IncomingMailServerUsername</key>
<string></string>
<key>OutgoingMailServerAuthentication</key>
<string>EmailAuthPassword</string>
<key>OutgoingMailServerHostName</key>
<string>$mail_fqdn</string>
<key>OutgoingMailServerPortNumber</key>
<integer>587</integer>
<key>OutgoingMailServerUseSSL</key>
<true/>
<key>OutgoingMailServerUsername</key>
<string></string>
<key>OutgoingPasswordSameAsIncomingPassword</key>
<true/>
<key>PayloadDescription</key>
<string>Email account configuration for $domain</string>
<key>PayloadDisplayName</key>
<string>$domain Email</string>
<key>PayloadIdentifier</key>
<string>com.$domain.email</string>
<key>PayloadType</key>
<string>com.apple.mail.managed</string>
<key>PayloadUUID</key>
<string>$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "12345678-1234-1234-1234-123456789012")</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>$domain Mail Configuration</string>
<key>PayloadIdentifier</key>
<string>com.$domain.profile</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>$(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "87654321-4321-4321-4321-210987654321")</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
EOF
log "Autoconfig files created in $autoconfig_dir"
# Setup DNS SRV records via dnsctl if available
if command -v dnsctl >/dev/null 2>&1; then
log "Setting up DNS autodiscover records..."
# Autoconfig/Autodiscover CNAME records
# Syntax: dnsctl add <TYPE> <subdomain> <target>
dnsctl add CNAME "autoconfig" "$mail_fqdn." 2>/dev/null || true
dnsctl add CNAME "autodiscover" "$mail_fqdn." 2>/dev/null || true
# SRV records for autodiscover (if supported by provider)
dnsctl add SRV "_autodiscover._tcp" "0 1 443 $mail_fqdn." 2>/dev/null || true
dnsctl add SRV "_imaps._tcp" "0 1 993 $mail_fqdn." 2>/dev/null || true
dnsctl add SRV "_submission._tcp" "0 1 587 $mail_fqdn." 2>/dev/null || true
log "DNS records configured"
else
warn "dnsctl not found - add DNS records manually:"
echo " autoconfig.$domain CNAME $mail_fqdn"
echo " autodiscover.$domain CNAME $mail_fqdn"
echo " _autodiscover._tcp.$domain SRV 0 1 443 $mail_fqdn"
echo " _imaps._tcp.$domain SRV 0 1 993 $mail_fqdn"
echo " _submission._tcp.$domain SRV 0 1 587 $mail_fqdn"
fi
# Register with HAProxy for autoconfig serving
if [ -x /usr/sbin/haproxyctl ]; then
log "Registering autoconfig with HAProxy..."
# Check if autoconfig vhost exists
local vhost_exists=$(uci show haproxy 2>/dev/null | grep "autoconfig_${domain//\./_}" || true)
if [ -z "$vhost_exists" ]; then
# Create a simple static file server for autoconfig
# HAProxy will serve these files
uci add haproxy vhost >/dev/null
uci set haproxy.@vhost[-1].name="autoconfig_${domain//\./_}"
uci set haproxy.@vhost[-1].domain="autoconfig.$domain"
uci set haproxy.@vhost[-1].backend="autoconfig_backend"
uci set haproxy.@vhost[-1].ssl='1'
uci set haproxy.@vhost[-1].acme='1'
uci set haproxy.@vhost[-1].enabled='1'
uci add haproxy vhost >/dev/null
uci set haproxy.@vhost[-1].name="autodiscover_${domain//\./_}"
uci set haproxy.@vhost[-1].domain="autodiscover.$domain"
uci set haproxy.@vhost[-1].backend="autoconfig_backend"
uci set haproxy.@vhost[-1].ssl='1'
uci set haproxy.@vhost[-1].acme='1'
uci set haproxy.@vhost[-1].enabled='1'
uci commit haproxy
log "HAProxy vhosts created for autoconfig.$domain and autodiscover.$domain"
fi
fi
log ""
log "Autoconfig setup complete!"
log ""
log "Thunderbird: https://autoconfig.$domain/mail/config-v1.1.xml"
log "Outlook: https://autodiscover.$domain/autodiscover/autodiscover.xml"
log "Apple: https://$mail_fqdn/$domain.mobileconfig"
log ""
log "DNS records needed:"
log " autoconfig.$domain CNAME $mail_fqdn"
log " autodiscover.$domain CNAME $mail_fqdn"
}
cmd_autoconfig_status() {
local domain=$(uci_get main.domain)
local hostname=$(uci_get main.hostname)
hostname="${hostname:-mail}"
local data_path=$(uci_get main.data_path)
data_path="${data_path:-/srv/mailserver}"
local autoconfig_dir="$data_path/autoconfig"
echo "Autoconfig Status for $domain"
echo "=============================="
echo ""
if [ -f "$autoconfig_dir/mail/config-v1.1.xml" ]; then
echo "Thunderbird config: OK"
else
echo "Thunderbird config: NOT FOUND"
fi
if [ -f "$autoconfig_dir/autodiscover/autodiscover.xml" ]; then
echo "Outlook config: OK"
else
echo "Outlook config: NOT FOUND"
fi
if [ -f "$autoconfig_dir/$domain.mobileconfig" ]; then
echo "Apple config: OK"
else
echo "Apple config: NOT FOUND"
fi
echo ""
echo "DNS Check:"
echo "----------"
host autoconfig.$domain 2>/dev/null | head -1 || echo "autoconfig.$domain: NOT RESOLVED"
host autodiscover.$domain 2>/dev/null | head -1 || echo "autodiscover.$domain: NOT RESOLVED"
}
# ============================================================================
# Firewall Setup
# ============================================================================
@ -693,6 +964,8 @@ Setup:
ssl-setup Obtain SSL certificate
firewall-setup Setup mail port forwarding (WAN only)
firewall-clear Remove mail firewall rules
autoconfig-setup Setup autodiscover for mail clients
autoconfig-status Check autoconfig status
Service:
start Start mail server
@ -769,6 +1042,8 @@ case "${1:-}" in
fix-ports) shift; cmd_fix_ports "$@" ;;
firewall-setup) shift; cmd_firewall_setup "$@" ;;
firewall-clear) shift; cmd_firewall_clear "$@" ;;
autoconfig-setup) shift; cmd_autoconfig_setup "$@" ;;
autoconfig-status) shift; cmd_autoconfig_status "$@" ;;
help|--help|-h|'') show_help ;;
*) error "Unknown command: $1"; show_help >&2; exit 1 ;;
esac