Skip to content

Preserve multiline SMS bodies when parsing CMGR/CMGL responses #17

Description

@Justinabox

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:

first line
second line

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions