Billionmail/BillionMail · error
retention_days must be a number
Error message
retention_days must be a number
What it means
RETENTION_DAYS must parse as an integer via strconv.Atoi; validateConfigValue rejects it otherwise. This value controls how long maillog/statistics data is retained, and a non-numeric value would break downstream numeric comparisons and cleanup jobs.
Source
Thrown at core/internal/controller/settings/settings.go:239
if !public.IsValidCIDR(value) {
return fmt.Errorf("IPv4 network format is incorrect, please use CIDR format (e.g. 192.168.1.0/24)")
}
case "TZ", "timezone":
// Timezone: check if it is a valid timezone
if !public.IsValidTimezone(value) {
return fmt.Errorf("invalid timezone")
}
case "FAIL2BAN_INIT", "fail2ban":
// fail2ban: only allowed y/n or 1/0
if value != "y" && value != "n" && value != "1" && value != "0" {
return fmt.Errorf("fail2ban value can only be y/n or 1/0")
}
case "RETENTION_DAYS", "retention_days":
// retention_days: must be a number
if _, err := strconv.Atoi(value); err != nil {
return fmt.Errorf("retention_days must be a number")
}
}
// General character check: not allowed dangerous characters
if public.ContainsDangerousChars(value) {
return fmt.Errorf("configuration value contains illegal characters")
}
return nil
}
View on GitHub (pinned to fc36c76c05)
Solutions
- Send a plain integer string, e.g. '30'
- Strip units and separators before the call ('30 days'→'30')
- Use whole days only — decimals are rejected by Atoi
- Ensure the field is not empty when the form is submitted
Example fix
// before RETENTION_DAYS = 30d // after RETENTION_DAYS = 30
Defensive patterns
Strategy: validation
Validate before calling
const n = Number(retentionDays);
if (!Number.isInteger(n) || n <= 0) throw new Error('retention_days must be a positive integer'); Type guard
function isWholeDays(v: unknown): v is number {
return typeof v === 'number' && Number.isInteger(v) && v > 0;
} Try / catch
try {
await api.setSystemConfigKey('RETENTION_DAYS', String(days));
} catch (e) {
if (String(e.message).includes('retention_days')) {
notify('Enter retention as a plain number of days, e.g. 30');
} else throw e;
} Prevention
- Send whole days as a plain integer, no units or decimals
- Strip thousands separators and whitespace
- Use a number input, not a text field
- Validate > 0 client-side
When it happens
Trigger: Setting RETENTION_DAYS to a non-numeric string such as '30d', 'thirty', '', '30 days', or a value with a decimal point like '30.5'.
Common situations: Users append units ('30d', '1 month') or paste values with whitespace; frontends send formatted numbers with thousands separators ('1,000').
Related errors
- hostname format is incorrect
- port must be between 1-65535
- IPv4 network format is incorrect, please use CIDR format (e.
- invalid timezone
- fail2ban value can only be y/n or 1/0
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/b765e5aef4cf3aa7.
Report an issue: GitHub.