sms.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. package device
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "strconv"
  7. "strings"
  8. "time"
  9. "vocat/internal/modem"
  10. )
  11. func (manager *Manager) SendSMS(
  12. ctx context.Context,
  13. id string,
  14. recipient string,
  15. text string,
  16. ) (SMSSendResult, error) {
  17. parts, err := prepareSMSParts(recipient, text)
  18. if err != nil {
  19. return SMSSendResult{}, err
  20. }
  21. result := SMSSendResult{
  22. To: parts[0].to,
  23. Encoding: parts[0].encoding,
  24. DeliveryConfirmed: false,
  25. DeliveryStatus: "unknown",
  26. SubmissionStatus: "unknown",
  27. SubmittedAt: time.Now().UTC(),
  28. PartsTotal: len(parts),
  29. PartResults: make([]SMSPartResult, 0, len(parts)),
  30. }
  31. if parts[0].concatReference != nil {
  32. reference := *parts[0].concatReference
  33. result.ConcatReference = &reference
  34. }
  35. state, err := manager.lookup(id)
  36. if err != nil {
  37. return result, err
  38. }
  39. state.opMu.Lock()
  40. defer state.opMu.Unlock()
  41. if err := manager.validateActive(id, state); err != nil {
  42. return result, err
  43. }
  44. client, err := manager.clientLocked(ctx, state, manager.candidateFor(state))
  45. if err != nil {
  46. manager.setResult(id, state, nil, err)
  47. return result, err
  48. }
  49. if err := manager.regionBlockError(state); err != nil {
  50. result.SubmissionStatus = "region_blocked"
  51. manager.setResult(id, state, nil, err)
  52. return result, err
  53. }
  54. for _, command := range parts[0].setup {
  55. if _, err := manager.command(ctx, client, command); err != nil {
  56. result.SubmissionStatus = "setup_failed"
  57. manager.setResult(id, state, nil, err)
  58. return result, err
  59. }
  60. }
  61. for _, part := range parts {
  62. response, submitErr := manager.prompt(
  63. ctx,
  64. client,
  65. part.prompt,
  66. part.payload,
  67. )
  68. reference, found := parseCMGSReference(response)
  69. partResult := SMSPartResult{
  70. Part: part.part,
  71. Total: part.total,
  72. MessageReference: reference,
  73. ReferenceKnown: found,
  74. AcceptedByModem: response.OK() && found,
  75. SubmissionStatus: "unknown",
  76. ModemFinal: response.Final,
  77. ModemEvidence: append([]string(nil), response.Lines...),
  78. SubmittedAt: time.Now().UTC(),
  79. }
  80. switch {
  81. case submitErr != nil && found:
  82. partResult.SubmissionStatus = "reference_returned_without_final"
  83. case submitErr != nil:
  84. var commandErr *modem.CommandError
  85. if errors.As(submitErr, &commandErr) {
  86. partResult.SubmissionStatus = "rejected_by_modem"
  87. }
  88. case !found:
  89. partResult.SubmissionStatus = "unconfirmed_without_reference"
  90. default:
  91. partResult.SubmissionStatus = "accepted_by_modem"
  92. }
  93. result.PartResults = append(result.PartResults, partResult)
  94. result.PartsAttempted++
  95. if partResult.AcceptedByModem {
  96. result.PartsAccepted++
  97. }
  98. result.ModemFinal = response.Final
  99. if len(parts) == 1 {
  100. result.MessageReference = reference
  101. result.ReferenceKnown = found
  102. result.AcceptedByModem = partResult.AcceptedByModem
  103. result.ModemEvidence = append([]string(nil), response.Lines...)
  104. } else {
  105. for _, line := range response.Lines {
  106. result.ModemEvidence = append(
  107. result.ModemEvidence,
  108. fmt.Sprintf("part %d/%d: %s", part.part, part.total, line),
  109. )
  110. }
  111. }
  112. if submitErr == nil && !found {
  113. submitErr = ErrSMSReferenceMissing
  114. }
  115. if submitErr == nil {
  116. continue
  117. }
  118. var commandErr *modem.CommandError
  119. switch {
  120. case len(parts) == 1:
  121. result.SubmissionStatus = partResult.SubmissionStatus
  122. case result.PartsAccepted > 0:
  123. result.SubmissionStatus = "partially_accepted_by_modem"
  124. case errors.As(submitErr, &commandErr):
  125. result.SubmissionStatus = "rejected_by_modem"
  126. default:
  127. result.SubmissionStatus = "unknown"
  128. }
  129. if errors.Is(submitErr, modem.ErrCommandTimeout) ||
  130. errors.Is(submitErr, context.Canceled) ||
  131. errors.Is(submitErr, context.DeadlineExceeded) {
  132. // A timeout after Ctrl-Z has an inherently uncertain outcome. Close
  133. // this session so a late +CMGS/OK cannot corrupt the next command;
  134. // callers must decide whether it is safe to retry.
  135. _ = client.Close()
  136. state.client = nil
  137. }
  138. partErr := fmt.Errorf(
  139. "submit SMS part %d/%d: %w",
  140. part.part,
  141. part.total,
  142. submitErr,
  143. )
  144. manager.setResult(id, state, nil, partErr)
  145. return result, partErr
  146. }
  147. result.AcceptedByModem = true
  148. result.AllPartsAccepted = true
  149. result.SubmissionStatus = "accepted_by_modem"
  150. manager.setResult(id, state, nil, nil)
  151. return result, nil
  152. }
  153. // smsListStorages are the message storage areas ListSMS enumerates. AT+CMGL
  154. // only reads the single storage currently selected by CPMS mem1, so a message
  155. // that landed in SIM storage (SM) is invisible while the module memory (ME) is
  156. // selected, and vice versa. Reading both guarantees nothing is missed regardless
  157. // of where the network or SIM placed it. MT is the union of SM and ME, so it is
  158. // not listed separately.
  159. var smsListStorages = []string{"SM", "ME"}
  160. func (manager *Manager) ListSMS(
  161. ctx context.Context,
  162. id string,
  163. ) ([]SMSMessage, error) {
  164. state, err := manager.lookup(id)
  165. if err != nil {
  166. return nil, err
  167. }
  168. state.opMu.Lock()
  169. defer state.opMu.Unlock()
  170. if err := manager.validateActive(id, state); err != nil {
  171. return nil, err
  172. }
  173. client, err := manager.clientLocked(ctx, state, manager.candidateFor(state))
  174. if err != nil {
  175. manager.setResult(id, state, nil, err)
  176. return nil, err
  177. }
  178. if _, err := manager.command(ctx, client, "AT+CMGF=0"); err != nil {
  179. manager.setResult(id, state, nil, err)
  180. return nil, err
  181. }
  182. var messages []SMSMessage
  183. var lastErr error
  184. listed := false
  185. for _, storage := range smsListStorages {
  186. // Select this storage for reading (mem1 only, so send/receive routing is
  187. // untouched). An unsupported storage reports an error; skip it rather than
  188. // fail the whole listing.
  189. if _, err := manager.command(
  190. ctx,
  191. client,
  192. fmt.Sprintf("AT+CPMS=%q", storage),
  193. ); err != nil {
  194. lastErr = err
  195. continue
  196. }
  197. response, err := manager.command(ctx, client, "AT+CMGL=4")
  198. if err != nil {
  199. lastErr = err
  200. continue
  201. }
  202. listed = true
  203. for _, message := range parseCMGL(response) {
  204. message.Storage = storage
  205. messages = append(messages, message)
  206. }
  207. }
  208. if !listed && lastErr != nil {
  209. manager.setResult(id, state, nil, lastErr)
  210. return nil, lastErr
  211. }
  212. manager.setResult(id, state, nil, nil)
  213. return messages, nil
  214. }
  215. func (manager *Manager) ReadSMS(
  216. ctx context.Context,
  217. id string,
  218. index int,
  219. ) (SMSMessage, error) {
  220. if index <= 0 {
  221. return SMSMessage{}, ErrSMSInvalidMessageIndex
  222. }
  223. state, err := manager.lookup(id)
  224. if err != nil {
  225. return SMSMessage{}, err
  226. }
  227. state.opMu.Lock()
  228. defer state.opMu.Unlock()
  229. if err := manager.validateActive(id, state); err != nil {
  230. return SMSMessage{}, err
  231. }
  232. client, err := manager.clientLocked(ctx, state, manager.candidateFor(state))
  233. if err != nil {
  234. manager.setResult(id, state, nil, err)
  235. return SMSMessage{}, err
  236. }
  237. if _, err := manager.command(ctx, client, "AT+CMGF=0"); err != nil {
  238. manager.setResult(id, state, nil, err)
  239. return SMSMessage{}, err
  240. }
  241. response, err := manager.command(
  242. ctx,
  243. client,
  244. fmt.Sprintf("AT+CMGR=%d", index),
  245. )
  246. if err != nil {
  247. manager.setResult(id, state, nil, err)
  248. return SMSMessage{}, err
  249. }
  250. message, err := parseCMGR(index, response)
  251. manager.setResult(id, state, nil, err)
  252. return message, err
  253. }
  254. func (manager *Manager) DeleteSMS(
  255. ctx context.Context,
  256. id string,
  257. index int,
  258. ) error {
  259. if index <= 0 {
  260. return ErrSMSInvalidMessageIndex
  261. }
  262. state, err := manager.lookup(id)
  263. if err != nil {
  264. return err
  265. }
  266. state.opMu.Lock()
  267. defer state.opMu.Unlock()
  268. if err := manager.validateActive(id, state); err != nil {
  269. return err
  270. }
  271. client, err := manager.clientLocked(ctx, state, manager.candidateFor(state))
  272. if err != nil {
  273. manager.setResult(id, state, nil, err)
  274. return err
  275. }
  276. _, err = manager.command(ctx, client, fmt.Sprintf("AT+CMGD=%d", index))
  277. manager.setResult(id, state, nil, err)
  278. return err
  279. }
  280. func (manager *Manager) prompt(
  281. ctx context.Context,
  282. client modem.Client,
  283. command string,
  284. payload []byte,
  285. ) (modem.Response, error) {
  286. promptClient, ok := client.(modem.PromptClient)
  287. if !ok {
  288. return modem.Response{}, ErrSMSPromptUnsupported
  289. }
  290. commandCtx, cancel := manager.withTimeout(ctx, manager.smsTimeout)
  291. defer cancel()
  292. response, err := promptClient.ExecutePrompt(commandCtx, command, payload)
  293. if err != nil {
  294. return response, fmt.Errorf("%s: %w", command, err)
  295. }
  296. return response, nil
  297. }
  298. func parseCMGSReference(response modem.Response) (int, bool) {
  299. value := valueAfterPrefix(response, "+CMGS:")
  300. values := csvValues(value)
  301. if len(values) == 0 {
  302. return 0, false
  303. }
  304. reference, err := strconv.Atoi(strings.TrimSpace(values[0]))
  305. if err != nil || reference < 0 || reference > 255 {
  306. return 0, false
  307. }
  308. return reference, true
  309. }
  310. type smsRecordHeader struct {
  311. index int
  312. status SMSStorageStatus
  313. modemLength int
  314. err error
  315. }
  316. func parseCMGL(response modem.Response) []SMSMessage {
  317. var result []SMSMessage
  318. for index := 0; index < len(response.Lines); {
  319. line := strings.TrimSpace(response.Lines[index])
  320. if !strings.HasPrefix(strings.ToUpper(line), "+CMGL:") {
  321. index++
  322. continue
  323. }
  324. header := parseCMGLHeader(line)
  325. index++
  326. rawPDU := ""
  327. if index < len(response.Lines) &&
  328. !strings.HasPrefix(
  329. strings.ToUpper(strings.TrimSpace(response.Lines[index])),
  330. "+CMGL:",
  331. ) {
  332. rawPDU = strings.TrimSpace(response.Lines[index])
  333. index++
  334. }
  335. message := SMSMessage{
  336. Index: header.index,
  337. StorageStatus: header.status,
  338. ModemLength: header.modemLength,
  339. Direction: SMSDirectionUnknown,
  340. Encoding: SMSEncodingUnknown,
  341. RawPDU: strings.ToUpper(rawPDU),
  342. }
  343. switch {
  344. case header.err != nil:
  345. message.DecodeError = header.err.Error()
  346. case rawPDU == "":
  347. message.DecodeError = "CMGL record has no PDU"
  348. default:
  349. decoded, decodeErr := decodeSMSPDU(rawPDU)
  350. decoded.Index = header.index
  351. decoded.StorageStatus = header.status
  352. decoded.ModemLength = header.modemLength
  353. message = decoded
  354. if decodeErr != nil {
  355. message.DecodeError = decodeErr.Error()
  356. }
  357. }
  358. result = append(result, message)
  359. }
  360. return result
  361. }
  362. func parseCMGLHeader(line string) smsRecordHeader {
  363. value := strings.TrimSpace(strings.SplitN(line, ":", 2)[1])
  364. values := csvValues(value)
  365. header := smsRecordHeader{status: SMSStatusUnknown}
  366. if len(values) < 2 {
  367. header.err = errors.New("invalid CMGL header")
  368. return header
  369. }
  370. var ok bool
  371. header.index, ok = parseDecimal(values[0])
  372. if !ok || header.index <= 0 {
  373. header.err = errors.New("invalid CMGL message index")
  374. }
  375. header.status = parseSMSStorageStatus(values[1])
  376. if len(values) >= 3 {
  377. if length, found := parseDecimal(values[len(values)-1]); found {
  378. header.modemLength = length
  379. }
  380. }
  381. return header
  382. }
  383. func parseCMGR(index int, response modem.Response) (SMSMessage, error) {
  384. for lineIndex, line := range response.Lines {
  385. line = strings.TrimSpace(line)
  386. if !strings.HasPrefix(strings.ToUpper(line), "+CMGR:") {
  387. continue
  388. }
  389. values := csvValues(strings.TrimSpace(strings.SplitN(line, ":", 2)[1]))
  390. if len(values) < 1 {
  391. return SMSMessage{}, errors.New("invalid CMGR header")
  392. }
  393. status := parseSMSStorageStatus(values[0])
  394. modemLength := 0
  395. if length, found := parseDecimal(values[len(values)-1]); found {
  396. modemLength = length
  397. }
  398. if lineIndex+1 >= len(response.Lines) {
  399. return SMSMessage{}, errors.New("CMGR response has no PDU")
  400. }
  401. message, decodeErr := decodeSMSPDU(response.Lines[lineIndex+1])
  402. message.Index = index
  403. message.StorageStatus = status
  404. message.ModemLength = modemLength
  405. if decodeErr != nil {
  406. message.DecodeError = decodeErr.Error()
  407. }
  408. // Return the raw record even when decoding is incomplete.
  409. return message, nil
  410. }
  411. return SMSMessage{}, errors.New("modem did not return a CMGR record")
  412. }
  413. func parseSMSStorageStatus(value string) SMSStorageStatus {
  414. value = strings.ToUpper(strings.Trim(strings.TrimSpace(value), `"`))
  415. switch value {
  416. case "0", "REC UNREAD":
  417. return SMSStatusReceivedUnread
  418. case "1", "REC READ":
  419. return SMSStatusReceivedRead
  420. case "2", "STO UNSENT":
  421. return SMSStatusStoredUnsent
  422. case "3", "STO SENT":
  423. return SMSStatusStoredSent
  424. default:
  425. return SMSStatusUnknown
  426. }
  427. }