Merge branch 'main' into copilot/fix-mailbox-connectivity-issues

This commit is contained in:
Christian Krakau-Louis
2026-05-04 00:21:59 +02:00
committed by GitHub
10 changed files with 80 additions and 4 deletions
+11
View File
@@ -33,6 +33,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
current streak has already been notified. When the account recovers the flag is current streak has already been notified. When the account recovers the flag is
cleared and a single recovery notification is sent; the next failure streak will cleared and a single recovery notification is sent; the next failure streak will
then fire a fresh alert. then fire a fresh alert.
## v0.10.2 (2026-05-03)
### Bug Fixes
- Add sender_email to UserSmtpConfig to fix SMTP From header (501 bad sender address)
([`a21e2f7`](https://github.com/christianlouis/InboxConverge/commit/a21e2f7df62ef79271efab172a88d1ead06bfe67))
## v0.10.1 (2026-05-03) ## v0.10.1 (2026-05-03)
@@ -44,6 +51,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## v0.10.0 (2026-05-03) ## v0.10.0 (2026-05-03)
### Bug Fixes
- **SMTP Fallback sender address**: Add `sender_email` field to `UserSmtpConfig` so the SMTP `From:` header uses a real email address instead of the SMTP authentication token. Providers like Postmark use a UUID API token as the username, which caused a `501 Bad sender address syntax` error. The new field is optional; when left blank the `username` value is used as a fallback (preserving existing behaviour for providers where username == email address). Includes a new Alembic migration (`0003`) and a Sender Email input in the Settings page.
## v0.9.2 (2026-05-03) ## v0.9.2 (2026-05-03)
@@ -0,0 +1,36 @@
"""Add sender_email column to user_smtp_configs
Revision ID: 0003
Revises: 0002
Create Date: 2026-05-03
Adds a ``sender_email`` column to ``user_smtp_configs``.
SMTP providers such as Postmark use an API token (UUID) as the SMTP username
for authentication, but require a real email address as the ``From:`` header.
This column stores the address that should appear as the sender; when blank,
the existing ``username`` value is used as a fallback so existing rows remain
fully functional.
Using IF NOT EXISTS makes the migration idempotent against fresh installs
where create_all() already created the column.
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = "0003"
down_revision = "0002"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"ALTER TABLE user_smtp_configs "
"ADD COLUMN IF NOT EXISTS sender_email VARCHAR(255) NOT NULL DEFAULT ''"
)
def downgrade() -> None:
op.execute("ALTER TABLE user_smtp_configs DROP COLUMN IF EXISTS sender_email")
+5 -1
View File
@@ -72,6 +72,7 @@ async def get_smtp_config(
host=config.host, # type: ignore[arg-type] host=config.host, # type: ignore[arg-type]
port=config.port, # type: ignore[arg-type] port=config.port, # type: ignore[arg-type]
username=config.username, # type: ignore[arg-type] username=config.username, # type: ignore[arg-type]
sender_email=config.sender_email, # type: ignore[arg-type]
use_tls=config.use_tls, # type: ignore[arg-type] use_tls=config.use_tls, # type: ignore[arg-type]
has_password=bool(config.encrypted_password), has_password=bool(config.encrypted_password),
created_at=config.created_at, # type: ignore[arg-type] created_at=config.created_at, # type: ignore[arg-type]
@@ -95,6 +96,7 @@ async def upsert_smtp_config(
config.host = config_in.host # type: ignore[assignment] config.host = config_in.host # type: ignore[assignment]
config.port = config_in.port # type: ignore[assignment] config.port = config_in.port # type: ignore[assignment]
config.username = config_in.username # type: ignore[assignment] config.username = config_in.username # type: ignore[assignment]
config.sender_email = config_in.sender_email # type: ignore[assignment]
config.use_tls = config_in.use_tls # type: ignore[assignment] config.use_tls = config_in.use_tls # type: ignore[assignment]
if config_in.password is not None: if config_in.password is not None:
config.encrypted_password = encrypt_credential(config_in.password) # type: ignore[assignment] config.encrypted_password = encrypt_credential(config_in.password) # type: ignore[assignment]
@@ -104,6 +106,7 @@ async def upsert_smtp_config(
host=config_in.host, host=config_in.host,
port=config_in.port, port=config_in.port,
username=config_in.username, username=config_in.username,
sender_email=config_in.sender_email,
encrypted_password=( encrypted_password=(
encrypt_credential(config_in.password) if config_in.password else "" encrypt_credential(config_in.password) if config_in.password else ""
), ),
@@ -120,6 +123,7 @@ async def upsert_smtp_config(
host=config.host, # type: ignore[arg-type] host=config.host, # type: ignore[arg-type]
port=config.port, # type: ignore[arg-type] port=config.port, # type: ignore[arg-type]
username=config.username, # type: ignore[arg-type] username=config.username, # type: ignore[arg-type]
sender_email=config.sender_email, # type: ignore[arg-type]
use_tls=config.use_tls, # type: ignore[arg-type] use_tls=config.use_tls, # type: ignore[arg-type]
has_password=bool(config.encrypted_password), has_password=bool(config.encrypted_password),
created_at=config.created_at, # type: ignore[arg-type] created_at=config.created_at, # type: ignore[arg-type]
@@ -174,7 +178,7 @@ async def test_smtp_config(
"plain", "plain",
"utf-8", "utf-8",
) )
msg["From"] = config.username # type: ignore[index] msg["From"] = config.sender_email or config.username # type: ignore[index]
msg["To"] = recipient msg["To"] = recipient
msg["Date"] = formatdate(localtime=True) msg["Date"] = formatdate(localtime=True)
msg["Message-ID"] = make_msgid() msg["Message-ID"] = make_msgid()
+1
View File
@@ -484,6 +484,7 @@ class UserSmtpConfig(Base):
username = Column(String(255), nullable=False, default="") username = Column(String(255), nullable=False, default="")
encrypted_password = Column(Text, nullable=False, default="") encrypted_password = Column(Text, nullable=False, default="")
use_tls = Column(Boolean, default=True) use_tls = Column(Boolean, default=True)
sender_email = Column(String(255), nullable=False, default="")
created_at = Column( created_at = Column(
DateTime(timezone=True), DateTime(timezone=True),
+1
View File
@@ -481,6 +481,7 @@ class UserSmtpConfigBase(BaseModel):
host: str = "smtp.gmail.com" host: str = "smtp.gmail.com"
port: int = Field(587, gt=0, lt=65536) port: int = Field(587, gt=0, lt=65536)
username: str = "" username: str = ""
sender_email: str = ""
use_tls: bool = True use_tls: bool = True
+3 -1
View File
@@ -1444,7 +1444,9 @@ class MailProcessor:
# Create forwarding message # Create forwarding message
forward_msg = MIMEMultipart("mixed") forward_msg = MIMEMultipart("mixed")
forward_msg["From"] = smtp_config["username"] forward_msg["From"] = (
smtp_config.get("sender_email") or smtp_config["username"]
)
forward_msg["To"] = destination forward_msg["To"] = destination
forward_msg["Date"] = formatdate(localtime=True) forward_msg["Date"] = formatdate(localtime=True)
forward_msg["Message-ID"] = make_msgid() forward_msg["Message-ID"] = make_msgid()
+1
View File
@@ -210,6 +210,7 @@ async def process_mail_account(account_id: int):
"host": user_smtp.host, "host": user_smtp.host,
"port": user_smtp.port, "port": user_smtp.port,
"username": user_smtp.username, "username": user_smtp.username,
"sender_email": user_smtp.sender_email or "",
"password": decrypt_credential(user_smtp.encrypted_password), # type: ignore[arg-type] "password": decrypt_credential(user_smtp.encrypted_password), # type: ignore[arg-type]
"use_tls": user_smtp.use_tls, "use_tls": user_smtp.use_tls,
} }
+19 -1
View File
@@ -148,6 +148,7 @@ function SettingsContent() {
host: 'smtp.gmail.com', host: 'smtp.gmail.com',
port: 587, port: 587,
username: '', username: '',
sender_email: '',
password: '', password: '',
use_tls: true, use_tls: true,
}); });
@@ -187,6 +188,7 @@ function SettingsContent() {
host: smtpConfig.host, host: smtpConfig.host,
port: smtpConfig.port, port: smtpConfig.port,
username: smtpConfig.username, username: smtpConfig.username,
sender_email: smtpConfig.sender_email,
use_tls: smtpConfig.use_tls, use_tls: smtpConfig.use_tls,
password: '', // never pre-fill password password: '', // never pre-fill password
})); }));
@@ -246,7 +248,7 @@ function SettingsContent() {
mutationFn: smtpApi.remove, mutationFn: smtpApi.remove,
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['smtp-config'] }); queryClient.invalidateQueries({ queryKey: ['smtp-config'] });
setSmtpForm({ host: 'smtp.gmail.com', port: 587, username: '', password: '', use_tls: true }); setSmtpForm({ host: 'smtp.gmail.com', port: 587, username: '', sender_email: '', password: '', use_tls: true });
}, },
}); });
@@ -302,6 +304,7 @@ function SettingsContent() {
host: smtpForm.host, host: smtpForm.host,
port: smtpForm.port, port: smtpForm.port,
username: smtpForm.username, username: smtpForm.username,
sender_email: smtpForm.sender_email,
password: smtpForm.password || undefined, password: smtpForm.password || undefined,
use_tls: smtpForm.use_tls, use_tls: smtpForm.use_tls,
}); });
@@ -601,6 +604,21 @@ function SettingsContent() {
/> />
</div> </div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Sender Email</label>
<input
type="email"
name="sender_email"
value={smtpForm.sender_email}
onChange={handleSmtpChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="sender@example.com"
/>
<p className="mt-1 text-xs text-gray-500">
The From: address used when forwarding mail. Required when your SMTP username is an API token rather than an email address (e.g. Postmark, SendGrid).
</p>
</div>
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Password</label> <label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
<input <input
+2
View File
@@ -224,6 +224,7 @@ export interface UserSmtpConfig {
host: string; host: string;
port: number; port: number;
username: string; username: string;
sender_email: string;
use_tls: boolean; use_tls: boolean;
has_password: boolean; has_password: boolean;
created_at: string; created_at: string;
@@ -485,6 +486,7 @@ export const smtpApi = {
host: string; host: string;
port: number; port: number;
username: string; username: string;
sender_email: string;
password?: string; password?: string;
use_tls: boolean; use_tls: boolean;
}): Promise<UserSmtpConfig> { }): Promise<UserSmtpConfig> {
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "inboxconverge" name = "inboxconverge"
version = "0.10.1" version = "0.10.2"
description = "Multi-account email forwarding and processing service" description = "Multi-account email forwarding and processing service"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"