Summary
Stored SMS parsing currently keeps only the first body line after a +CMGR/+CMGL header. If a real inbound SMS contains embedded newlines, SMSService.read_message() and SMSService.list_messages() truncate the body at the first line; extra body lines are skipped or ignored.
This is a concrete SMS robustness gap for MFA/automation messages that include line breaks, carrier templates, or human replies with multiple paragraphs.
Evidence
Affected code:
callstack/sms/service.py:348-352 takes only lines[i + 1] as the +CMGL body.
callstack/sms/service.py:375-377 takes only lines[i + 1] as the +CMGR body.
tests/test_sms_service.py:219-255 covers only single-line bodies for list/read paths.
Minimal reproduction on main (1c883c59575676fe6ce28214f27aeded692769b8):
PYTHONPATH=. uv run --no-project --with aiohttp --with pyserial-asyncio python - <<'PY'
from callstack.sms.service import SMSService
single_lines = [
'+CMGR: "REC UNREAD","+155****1234","","24/12/25,14:30:00+04"',
'first line',
'second line',
'OK',
]
sms = SMSService._parse_single_message(single_lines, 7)
print('CMGR multiline parsed body:', repr(sms.body if sms else None))
list_lines = [
'+CMGL: 0,"REC UNREAD","+155****1111","","24/12/25,10:00:00+04"',
'first line',
'second line',
'+CMGL: 1,"REC READ","+155****2222","","24/12/25,11:00:00+04"',
'world',
'OK',
]
msgs = SMSService._parse_message_list(list_lines)
print('CMGL message count:', len(msgs))
print('CMGL bodies:', [repr(m.body) for m in msgs])
PY
Actual output:
CMGR multiline parsed body: 'first line'
CMGL message count: 2
CMGL bodies: ["'first line'", "'world'"]
The second line of message 0 is lost.
Repository health checks from this scout run:
git diff --check
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q
# 307 passed in 4.24s
Duplicate searches run before filing:
gh issue list --state all --search "CMGR multiline SMS body line truncates"
gh issue list --state all --search "CMGL multiline SMS body line truncates"
gh pr list --state all --search "CMGR multiline SMS body line truncates"
gh pr list --state all --search "CMGL multiline SMS body line truncates"
# no results
Expected behavior
read_message() and list_messages() should preserve the complete SMS text returned by the modem, including embedded newlines, until the next SMS header or final result code.
For the reproduction above, the first body should be equivalent to:
Actual behavior
Only the first line after the header is stored in SMS.body; subsequent body lines are skipped/ignored.
Suggested fix direction
- Replace the single-
i + 1 body extraction with response-block parsing:
- A body starts after a
+CMGR/+CMGL header.
- Continue collecting body lines until the next
+CMGL: header or a final result/error line.
- Join collected body lines with
\n so the public body matches user-visible SMS content.
- Add regression tests for multiline
+CMGR and +CMGL bodies.
- Keep the parser compatible with one-line bodies and with existing multipart UDH metadata work.
Acceptance criteria
SMSService._parse_single_message() preserves multiline message bodies.
SMSService._parse_message_list() preserves multiline message bodies and still separates adjacent +CMGL records correctly.
- Existing single-line SMS tests continue to pass.
- No SIM numbers or message contents are logged beyond existing masked/test fixtures.
Verification gates
git diff --check
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q
Summary
Stored SMS parsing currently keeps only the first body line after a
+CMGR/+CMGLheader. If a real inbound SMS contains embedded newlines,SMSService.read_message()andSMSService.list_messages()truncate the body at the first line; extra body lines are skipped or ignored.This is a concrete SMS robustness gap for MFA/automation messages that include line breaks, carrier templates, or human replies with multiple paragraphs.
Evidence
Affected code:
callstack/sms/service.py:348-352takes onlylines[i + 1]as the+CMGLbody.callstack/sms/service.py:375-377takes onlylines[i + 1]as the+CMGRbody.tests/test_sms_service.py:219-255covers only single-line bodies for list/read paths.Minimal reproduction on
main(1c883c59575676fe6ce28214f27aeded692769b8):Actual output:
The second line of message 0 is lost.
Repository health checks from this scout run:
git diff --check PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q # 307 passed in 4.24sDuplicate searches run before filing:
Expected behavior
read_message()andlist_messages()should preserve the complete SMS text returned by the modem, including embedded newlines, until the next SMS header or final result code.For the reproduction above, the first body should be equivalent to:
Actual behavior
Only the first line after the header is stored in
SMS.body; subsequent body lines are skipped/ignored.Suggested fix direction
i + 1body extraction with response-block parsing:+CMGR/+CMGLheader.+CMGL:header or a final result/error line.\nso the public body matches user-visible SMS content.+CMGRand+CMGLbodies.Acceptance criteria
SMSService._parse_single_message()preserves multiline message bodies.SMSService._parse_message_list()preserves multiline message bodies and still separates adjacent+CMGLrecords correctly.Verification gates