diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index 16d9097f..8ec82d03 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -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` diff --git a/package/secubox/luci-app-mailserver/htdocs/luci-static/resources/view/mailserver/overview.js b/package/secubox/luci-app-mailserver/htdocs/luci-static/resources/view/mailserver/overview.js index e8670be0..ac6ddcca 100644 --- a/package/secubox/luci-app-mailserver/htdocs/luci-static/resources/view/mailserver/overview.js +++ b/package/secubox/luci-app-mailserver/htdocs/luci-static/resources/view/mailserver/overview.js @@ -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...') diff --git a/package/secubox/luci-app-mailserver/root/usr/libexec/rpcd/luci.mailserver b/package/secubox/luci-app-mailserver/root/usr/libexec/rpcd/luci.mailserver index 981cbdd4..de04774b 100644 --- a/package/secubox/luci-app-mailserver/root/usr/libexec/rpcd/luci.mailserver +++ b/package/secubox/luci-app-mailserver/root/usr/libexec/rpcd/luci.mailserver @@ -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 diff --git a/package/secubox/luci-app-mailserver/root/usr/share/rpcd/acl.d/luci-app-mailserver.json b/package/secubox/luci-app-mailserver/root/usr/share/rpcd/acl.d/luci-app-mailserver.json index 740eeffd..bd6d1a7a 100644 --- a/package/secubox/luci-app-mailserver/root/usr/share/rpcd/acl.d/luci-app-mailserver.json +++ b/package/secubox/luci-app-mailserver/root/usr/share/rpcd/acl.d/luci-app-mailserver.json @@ -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"] } diff --git a/package/secubox/luci-app-nextcloud/root/usr/libexec/rpcd/luci.nextcloud b/package/secubox/luci-app-nextcloud/root/usr/libexec/rpcd/luci.nextcloud index a5b7e4e0..da12cb2b 100755 --- a/package/secubox/luci-app-nextcloud/root/usr/libexec/rpcd/luci.nextcloud +++ b/package/secubox/luci-app-nextcloud/root/usr/libexec/rpcd/luci.nextcloud @@ -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 ;; diff --git a/package/secubox/luci-app-nextcloud/root/usr/share/rpcd/acl.d/luci-app-nextcloud.json b/package/secubox/luci-app-nextcloud/root/usr/share/rpcd/acl.d/luci-app-nextcloud.json index 916582ff..36ff9cc6 100644 --- a/package/secubox/luci-app-nextcloud/root/usr/share/rpcd/acl.d/luci-app-nextcloud.json +++ b/package/secubox/luci-app-nextcloud/root/usr/share/rpcd/acl.d/luci-app-nextcloud.json @@ -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"] diff --git a/package/secubox/secubox-app-mailserver/files/usr/sbin/mailctl b/package/secubox/secubox-app-mailserver/files/usr/sbin/mailctl index 565541ff..6e040e3d 100644 --- a/package/secubox/secubox-app-mailserver/files/usr/sbin/mailctl +++ b/package/secubox/secubox-app-mailserver/files/usr/sbin/mailctl @@ -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 + + + + $domain + $domain Mail + $domain + + $mail_fqdn + 993 + SSL + password-cleartext + %EMAILADDRESS% + + + $mail_fqdn + 143 + STARTTLS + password-cleartext + %EMAILADDRESS% + + + $mail_fqdn + 587 + STARTTLS + password-cleartext + %EMAILADDRESS% + + + $mail_fqdn + 465 + SSL + password-cleartext + %EMAILADDRESS% + + + +EOF + + # Outlook/ActiveSync autodiscover (autodiscover.xml) + cat > "$autoconfig_dir/autodiscover/autodiscover.xml" << EOF + + + + + email + settings + + IMAP + $mail_fqdn + 993 + off + + off + on + on + + + SMTP + $mail_fqdn + 587 + off + + off + TLS + on + on + off + + + + +EOF + + # Apple mobileconfig profile + cat > "$autoconfig_dir/$domain.mobileconfig" << EOF + + + + + PayloadContent + + + EmailAccountDescription + $domain Mail + EmailAccountName + $domain + EmailAccountType + EmailTypeIMAP + EmailAddress + + IncomingMailServerAuthentication + EmailAuthPassword + IncomingMailServerHostName + $mail_fqdn + IncomingMailServerPortNumber + 993 + IncomingMailServerUseSSL + + IncomingMailServerUsername + + OutgoingMailServerAuthentication + EmailAuthPassword + OutgoingMailServerHostName + $mail_fqdn + OutgoingMailServerPortNumber + 587 + OutgoingMailServerUseSSL + + OutgoingMailServerUsername + + OutgoingPasswordSameAsIncomingPassword + + PayloadDescription + Email account configuration for $domain + PayloadDisplayName + $domain Email + PayloadIdentifier + com.$domain.email + PayloadType + com.apple.mail.managed + PayloadUUID + $(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "12345678-1234-1234-1234-123456789012") + PayloadVersion + 1 + + + PayloadDisplayName + $domain Mail Configuration + PayloadIdentifier + com.$domain.profile + PayloadType + Configuration + PayloadUUID + $(cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "87654321-4321-4321-4321-210987654321") + PayloadVersion + 1 + + +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 + 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