@cakemail-org/cakemail-cli 1.5.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/.env.example +40 -0
  3. package/.env.test.example +45 -0
  4. package/CHANGELOG.md +1031 -0
  5. package/README.md +319 -15
  6. package/audit-formats.js +128 -0
  7. package/cakemail.rb +20 -0
  8. package/dist/cli.js +27 -10
  9. package/dist/cli.js.map +1 -1
  10. package/dist/client.d.ts +2 -0
  11. package/dist/client.d.ts.map +1 -1
  12. package/dist/client.js +16 -6
  13. package/dist/client.js.map +1 -1
  14. package/dist/commands/account.js +1 -1
  15. package/dist/commands/account.js.map +1 -1
  16. package/dist/commands/attributes.js +1 -1
  17. package/dist/commands/attributes.js.map +1 -1
  18. package/dist/commands/campaigns.d.ts.map +1 -1
  19. package/dist/commands/campaigns.js +103 -8
  20. package/dist/commands/campaigns.js.map +1 -1
  21. package/dist/commands/config.d.ts.map +1 -1
  22. package/dist/commands/config.js +63 -4
  23. package/dist/commands/config.js.map +1 -1
  24. package/dist/commands/contacts.d.ts.map +1 -1
  25. package/dist/commands/contacts.js +91 -12
  26. package/dist/commands/contacts.js.map +1 -1
  27. package/dist/commands/emails.js +1 -1
  28. package/dist/commands/emails.js.map +1 -1
  29. package/dist/commands/interests.d.ts +5 -0
  30. package/dist/commands/interests.d.ts.map +1 -0
  31. package/dist/commands/interests.js +172 -0
  32. package/dist/commands/interests.js.map +1 -0
  33. package/dist/commands/lists.d.ts.map +1 -1
  34. package/dist/commands/lists.js +6 -8
  35. package/dist/commands/lists.js.map +1 -1
  36. package/dist/commands/logs.d.ts +5 -0
  37. package/dist/commands/logs.d.ts.map +1 -0
  38. package/dist/commands/logs.js +237 -0
  39. package/dist/commands/logs.js.map +1 -0
  40. package/dist/commands/reports.js +1 -1
  41. package/dist/commands/reports.js.map +1 -1
  42. package/dist/commands/segments.js +1 -1
  43. package/dist/commands/segments.js.map +1 -1
  44. package/dist/commands/senders.d.ts.map +1 -1
  45. package/dist/commands/senders.js +11 -8
  46. package/dist/commands/senders.js.map +1 -1
  47. package/dist/commands/suppressed.js +1 -1
  48. package/dist/commands/suppressed.js.map +1 -1
  49. package/dist/commands/tags.d.ts +5 -0
  50. package/dist/commands/tags.d.ts.map +1 -0
  51. package/dist/commands/tags.js +124 -0
  52. package/dist/commands/tags.js.map +1 -0
  53. package/dist/commands/templates.js +1 -1
  54. package/dist/commands/templates.js.map +1 -1
  55. package/dist/commands/transactional-templates.d.ts +5 -0
  56. package/dist/commands/transactional-templates.d.ts.map +1 -0
  57. package/dist/commands/transactional-templates.js +354 -0
  58. package/dist/commands/transactional-templates.js.map +1 -0
  59. package/dist/commands/webhooks.js +1 -1
  60. package/dist/commands/webhooks.js.map +1 -1
  61. package/dist/utils/auth.d.ts +8 -1
  62. package/dist/utils/auth.d.ts.map +1 -1
  63. package/dist/utils/auth.js +39 -11
  64. package/dist/utils/auth.js.map +1 -1
  65. package/dist/utils/config-file.d.ts +7 -0
  66. package/dist/utils/config-file.d.ts.map +1 -1
  67. package/dist/utils/config-file.js +15 -0
  68. package/dist/utils/config-file.js.map +1 -1
  69. package/dist/utils/config.d.ts +2 -0
  70. package/dist/utils/config.d.ts.map +1 -1
  71. package/dist/utils/config.js +12 -4
  72. package/dist/utils/config.js.map +1 -1
  73. package/dist/utils/errors.js +1 -1
  74. package/dist/utils/errors.js.map +1 -1
  75. package/dist/utils/list-defaults.d.ts +33 -0
  76. package/dist/utils/list-defaults.d.ts.map +1 -0
  77. package/dist/utils/list-defaults.js +52 -0
  78. package/dist/utils/list-defaults.js.map +1 -0
  79. package/dist/utils/output.d.ts.map +1 -1
  80. package/dist/utils/output.js +36 -13
  81. package/dist/utils/output.js.map +1 -1
  82. package/dist/utils/progress.d.ts.map +1 -1
  83. package/dist/utils/progress.js +32 -4
  84. package/dist/utils/progress.js.map +1 -1
  85. package/dist/utils/spinner.d.ts +17 -0
  86. package/dist/utils/spinner.d.ts.map +1 -0
  87. package/dist/utils/spinner.js +43 -0
  88. package/dist/utils/spinner.js.map +1 -0
  89. package/docs/DOCUMENTATION-STANDARD.md +1068 -0
  90. package/docs/README.md +161 -0
  91. package/docs/developer/ARCHITECTURE.md +516 -0
  92. package/docs/developer/AUTH.md +204 -0
  93. package/docs/developer/CONTRIBUTING.md +227 -0
  94. package/docs/developer/DOCUMENTATION_SUMMARY.md +346 -0
  95. package/docs/developer/PROJECT_INDEX.md +365 -0
  96. package/docs/planning/API_COVERAGE.md +1045 -0
  97. package/docs/planning/BACKLOG.md +1159 -0
  98. package/docs/planning/PROFILE_SYSTEM_TASKS.md +287 -0
  99. package/docs/planning/UX_IMPLEMENTATION_PLAN.md +691 -0
  100. package/docs/planning/archive/RELEASE_CHECKLIST_v1.3.0.md +332 -0
  101. package/docs/planning/archive/RELEASE_v1.3.0.md +428 -0
  102. package/docs/planning/archive/cakemail-cli-ux-improvements.md +438 -0
  103. package/docs/planning/cakemail-profile-system-plan.md +1121 -0
  104. package/docs/testing/AI_USER_SIMULATION_DESIGN.md +1342 -0
  105. package/docs/testing/KENOGAMI_BIDIRECTIONAL_FLOW.md +1517 -0
  106. package/docs/testing/KENOGAMI_TRUTH_RECONCILIATION_SYSTEM.md +1369 -0
  107. package/docs/user-manual/.obsidian/app.json +1 -0
  108. package/docs/user-manual/.obsidian/appearance.json +1 -0
  109. package/docs/user-manual/.obsidian/core-plugins.json +33 -0
  110. package/docs/user-manual/.obsidian/workspace.json +167 -0
  111. package/docs/user-manual/01-getting-started/01-installation.md +214 -0
  112. package/docs/user-manual/01-getting-started/02-quick-start.md +432 -0
  113. package/docs/user-manual/01-getting-started/03-authentication.md +448 -0
  114. package/docs/user-manual/01-getting-started/04-configuration.md +430 -0
  115. package/docs/user-manual/01-getting-started/05-output-formats.md +447 -0
  116. package/docs/user-manual/02-core-concepts/01-accounts.md +514 -0
  117. package/docs/user-manual/02-core-concepts/02-profile-system.md +771 -0
  118. package/docs/user-manual/02-core-concepts/03-smart-defaults.md +485 -0
  119. package/docs/user-manual/02-core-concepts/04-authentication-methods.md +435 -0
  120. package/docs/user-manual/02-core-concepts/05-pagination-filtering.md +600 -0
  121. package/docs/user-manual/02-core-concepts/06-error-handling.md +718 -0
  122. package/docs/user-manual/02-core-concepts/07-api-coverage.md +483 -0
  123. package/docs/user-manual/03-email-operations/01-senders.md +490 -0
  124. package/docs/user-manual/03-email-operations/02-templates.md +444 -0
  125. package/docs/user-manual/03-email-operations/03-transactional-emails.md +706 -0
  126. package/docs/user-manual/03-email-operations/04-email-tracking.md +407 -0
  127. package/docs/user-manual/04-campaign-management/01-campaigns-basics.md +394 -0
  128. package/docs/user-manual/04-campaign-management/02-campaign-scheduling.md +630 -0
  129. package/docs/user-manual/04-campaign-management/03-campaign-testing.md +997 -0
  130. package/docs/user-manual/04-campaign-management/04-campaign-lifecycle.md +709 -0
  131. package/docs/user-manual/04-campaign-management/05-campaign-links.md +934 -0
  132. package/docs/user-manual/05-contact-management/01-lists.md +836 -0
  133. package/docs/user-manual/05-contact-management/02-contacts.md +1035 -0
  134. package/docs/user-manual/05-contact-management/03-custom-attributes.md +788 -0
  135. package/docs/user-manual/05-contact-management/04-segments.md +1028 -0
  136. package/docs/user-manual/05-contact-management/05-contact-import-export.md +1031 -0
  137. package/docs/user-manual/06-analytics-reporting/01-campaign-analytics.md +867 -0
  138. package/docs/user-manual/06-analytics-reporting/02-account-reports.md +227 -0
  139. package/docs/user-manual/07-integrations/01-webhooks-integration.md +259 -0
  140. package/docs/user-manual/07-integrations/02-automation.md +326 -0
  141. package/docs/user-manual/08-advanced-usage/01-scripting-patterns.md +672 -0
  142. package/docs/user-manual/08-advanced-usage/02-bulk-operations.md +932 -0
  143. package/docs/user-manual/08-advanced-usage/03-ci-cd-integration.md +892 -0
  144. package/docs/user-manual/08-advanced-usage/04-performance-optimization.md +766 -0
  145. package/docs/user-manual/09-command-reference/01-config.md +776 -0
  146. package/docs/user-manual/09-command-reference/02-account.md +652 -0
  147. package/docs/user-manual/09-command-reference/03-lists.md +958 -0
  148. package/docs/user-manual/09-command-reference/04-contacts.md +1408 -0
  149. package/docs/user-manual/09-command-reference/05-attributes.md +617 -0
  150. package/docs/user-manual/09-command-reference/06-segments.md +894 -0
  151. package/docs/user-manual/09-command-reference/07-senders.md +803 -0
  152. package/docs/user-manual/09-command-reference/08-templates.md +818 -0
  153. package/docs/user-manual/09-command-reference/09-campaigns.md +1250 -0
  154. package/docs/user-manual/09-command-reference/10-emails.md +807 -0
  155. package/docs/user-manual/09-command-reference/11-reports.md +1135 -0
  156. package/docs/user-manual/09-command-reference/12-webhooks.md +773 -0
  157. package/docs/user-manual/09-command-reference/13-suppressed.md +797 -0
  158. package/docs/user-manual/09-command-reference/14-interests.md +630 -0
  159. package/docs/user-manual/09-command-reference/15-tags.md +584 -0
  160. package/docs/user-manual/09-command-reference/16-logs.md +656 -0
  161. package/docs/user-manual/09-command-reference/17-transactional-templates.md +850 -0
  162. package/docs/user-manual/10-troubleshooting/01-common-errors.md +457 -0
  163. package/docs/user-manual/10-troubleshooting/02-authentication-issues.md +558 -0
  164. package/docs/user-manual/10-troubleshooting/03-connection-problems.md +634 -0
  165. package/docs/user-manual/10-troubleshooting/04-debugging.md +725 -0
  166. package/docs/user-manual/11-appendix/04-faq.md +484 -0
  167. package/docs/user-manual/11-appendix/05-glossary.md +250 -0
  168. package/docs/user-manual/README.md +0 -0
  169. package/package.json +13 -47
  170. package/src/cli.ts +125 -0
  171. package/src/client.ts +16 -0
  172. package/src/commands/account.ts +267 -0
  173. package/src/commands/accounts.ts +78 -0
  174. package/src/commands/actions.ts +249 -0
  175. package/src/commands/attributes.ts +139 -0
  176. package/src/commands/campaign-blueprints.ts +106 -0
  177. package/src/commands/campaigns.ts +469 -0
  178. package/src/commands/config.ts +77 -0
  179. package/src/commands/contacts.ts +612 -0
  180. package/src/commands/custom-attributes.ts +127 -0
  181. package/src/commands/dkims.ts +117 -0
  182. package/src/commands/domains.ts +82 -0
  183. package/src/commands/email-apis.ts +569 -0
  184. package/src/commands/emails.ts +197 -0
  185. package/src/commands/forms.ts +283 -0
  186. package/src/commands/interests.ts +155 -0
  187. package/src/commands/links.ts +38 -0
  188. package/src/commands/lists.ts +406 -0
  189. package/src/commands/logos.ts +71 -0
  190. package/src/commands/logs.ts +386 -0
  191. package/src/commands/reports.ts +306 -0
  192. package/src/commands/segments.ts +158 -0
  193. package/src/commands/senders.ts +204 -0
  194. package/src/commands/sub-accounts.ts +271 -0
  195. package/src/commands/suppressed-emails.ts +234 -0
  196. package/src/commands/suppressed.ts +198 -0
  197. package/src/commands/system-emails.ts +85 -0
  198. package/src/commands/tags.ts +146 -0
  199. package/src/commands/tasks.ts +116 -0
  200. package/src/commands/templates.ts +189 -0
  201. package/src/commands/tokens.ts +83 -0
  202. package/src/commands/transactional-emails.ts +374 -0
  203. package/src/commands/transactional-templates.ts +385 -0
  204. package/src/commands/users.ts +506 -0
  205. package/src/commands/webhooks.ts +172 -0
  206. package/src/commands/workflow-blueprints.ts +123 -0
  207. package/src/commands/workflows.ts +265 -0
  208. package/src/types/profile.ts +93 -0
  209. package/src/utils/auth.ts +272 -0
  210. package/src/utils/config-file.ts +96 -0
  211. package/src/utils/config.ts +134 -0
  212. package/src/utils/confirm.ts +32 -0
  213. package/src/utils/defaults.ts +99 -0
  214. package/src/utils/errors.ts +116 -0
  215. package/src/utils/interactive.ts +91 -0
  216. package/src/utils/list-defaults.ts +74 -0
  217. package/src/utils/output.ts +190 -0
  218. package/src/utils/progress.ts +320 -0
  219. package/src/utils/spinner.ts +22 -0
  220. package/tests/IMPLEMENTATION_STATUS.md +258 -0
  221. package/tests/PTY_SETUP.md +118 -0
  222. package/tests/PTY_TESTING_GUIDE.md +507 -0
  223. package/tests/README.md +244 -0
  224. package/tests/fixtures/api-responses/campaigns.json +34 -0
  225. package/tests/fixtures/test-config.json +13 -0
  226. package/tests/helpers/cli-runner.ts +128 -0
  227. package/tests/helpers/mock-server.ts +301 -0
  228. package/tests/helpers/pty-runner.ts +181 -0
  229. package/tests/integration/campaigns-real-api.test.ts +196 -0
  230. package/tests/integration/setup-integration.ts +50 -0
  231. package/tests/pty/campaigns.test.ts +241 -0
  232. package/tests/setup.ts +34 -0
  233. package/tsconfig.json +15 -0
  234. package/vitest.config.ts +28 -0
@@ -0,0 +1,1035 @@
1
+ # Managing Contacts
2
+
3
+ Master individual contact management including adding, updating, searching, and tracking contact data.
4
+
5
+ ## Overview
6
+
7
+ Contact management allows you to:
8
+ - Add individual contacts to lists
9
+ - Update contact information
10
+ - Track custom attributes and preferences
11
+ - Search and filter contacts
12
+ - View contact engagement history
13
+ - Manage subscription status
14
+ - Delete contacts when needed
15
+
16
+ Every contact belongs to at least one list and can have standard fields (email, name) plus custom attributes for your specific data needs.
17
+
18
+ ## Quick Start
19
+
20
+ ### Add Your First Contact
21
+
22
+ ```bash
23
+ $ cakemail contacts add 123 -e "john@example.com" -f "John" -l "Doe"
24
+ ```
25
+
26
+ **Output:**
27
+ ```
28
+ ✓ Contact added successfully
29
+
30
+ ID: 501
31
+ Email: john@example.com
32
+ Name: John Doe
33
+ Status: subscribed
34
+ List: 123
35
+ ```
36
+
37
+ ### View Contact Details
38
+
39
+ ```bash
40
+ $ cakemail contacts get 123 501
41
+ ```
42
+
43
+ **Output:**
44
+ ```json
45
+ {
46
+ "id": 501,
47
+ "email": "john@example.com",
48
+ "first_name": "John",
49
+ "last_name": "Doe",
50
+ "status": "subscribed",
51
+ "subscribed_on": "2024-03-15T10:30:00Z",
52
+ "last_bounce": null,
53
+ "bounces_count": 0,
54
+ "custom_attributes": {}
55
+ }
56
+ ```
57
+
58
+ ## Contact Management Basics
59
+
60
+ ### Add Contact
61
+
62
+ **Simple contact:**
63
+ ```bash
64
+ $ cakemail contacts add 123 -e "jane@example.com"
65
+ ```
66
+
67
+ **With name:**
68
+ ```bash
69
+ $ cakemail contacts add 123 \
70
+ -e "jane@example.com" \
71
+ -f "Jane" \
72
+ -l "Smith"
73
+ ```
74
+
75
+ **With custom attributes:**
76
+ ```bash
77
+ $ cakemail contacts add 123 \
78
+ -e "jane@example.com" \
79
+ -f "Jane" \
80
+ -l "Smith" \
81
+ -d '{"plan":"premium","signup_date":"2024-03-15","is_vip":true}'
82
+ ```
83
+
84
+ **Auto-detect list:**
85
+ ```bash
86
+ # If you have only one list, list ID is optional
87
+ $ cakemail contacts add -e "new@example.com" -f "New" -l "User"
88
+ ```
89
+
90
+ **Output:**
91
+ ```
92
+ ✓ Auto-detected list: 123 (Newsletter Subscribers)
93
+ ✓ Contact added successfully
94
+ ```
95
+
96
+ ### View Contact
97
+
98
+ ```bash
99
+ $ cakemail contacts get 123 501
100
+ ```
101
+
102
+ **Extract specific fields:**
103
+ ```bash
104
+ $ cakemail contacts get 123 501 -f json | jq '{email, status, custom_attributes}'
105
+ ```
106
+
107
+ **Output:**
108
+ ```json
109
+ {
110
+ "email": "john@example.com",
111
+ "status": "subscribed",
112
+ "custom_attributes": {
113
+ "plan": "premium",
114
+ "signup_date": "2024-03-15",
115
+ "is_vip": true
116
+ }
117
+ }
118
+ ```
119
+
120
+ ### Update Contact
121
+
122
+ **Update name:**
123
+ ```bash
124
+ $ cakemail contacts update 123 501 -f "Jonathan" -l "Doe"
125
+ ```
126
+
127
+ **Update email:**
128
+ ```bash
129
+ $ cakemail contacts update 123 501 -e "newemail@example.com"
130
+ ```
131
+
132
+ **Update custom attributes:**
133
+ ```bash
134
+ $ cakemail contacts update 123 501 -d '{"plan":"enterprise","is_vip":true}'
135
+ ```
136
+
137
+ **Update multiple fields:**
138
+ ```bash
139
+ $ cakemail contacts update 123 501 \
140
+ -f "Jonathan" \
141
+ -l "Doe" \
142
+ -e "j.doe@example.com" \
143
+ -d '{"plan":"enterprise","lifetime_value":599.99}'
144
+ ```
145
+
146
+ ### Delete Contact
147
+
148
+ ```bash
149
+ $ cakemail contacts delete 123 501
150
+ ```
151
+
152
+ **Output:**
153
+ ```
154
+ ⚠ Delete contact 501?
155
+
156
+ The following will happen:
157
+ • Contact will be permanently deleted
158
+ • All engagement history lost
159
+ • Cannot be recovered
160
+
161
+ Type 'yes' to confirm: yes
162
+
163
+ ✓ Contact 501 deleted
164
+ ```
165
+
166
+ **Force delete (skip confirmation):**
167
+ ```bash
168
+ $ cakemail contacts delete 123 501 --force
169
+ ```
170
+
171
+ ## Searching Contacts
172
+
173
+ ### List All Contacts
174
+
175
+ ```bash
176
+ $ cakemail contacts list 123
177
+ ```
178
+
179
+ **Output:**
180
+ ```
181
+ ┌──────┬────────────────────────┬───────────────┬────────────┬─────────────────────┐
182
+ │ ID │ Email │ Name │ Status │ Subscribed │
183
+ ├──────┼────────────────────────┼───────────────┼────────────┼─────────────────────┤
184
+ │ 501 │ john@example.com │ John Doe │ subscribed │ 2024-01-15 10:30:00 │
185
+ │ 502 │ jane@example.com │ Jane Smith │ subscribed │ 2024-01-16 14:20:00 │
186
+ │ 503 │ bob@example.com │ Bob Johnson │ subscribed │ 2024-01-17 09:15:00 │
187
+ └──────┴────────────────────────┴───────────────┴────────────┴─────────────────────┘
188
+
189
+ Total: 3 contacts
190
+ ```
191
+
192
+ ### Search by Email
193
+
194
+ ```bash
195
+ $ cakemail contacts list 123 --filter "email==john@example.com"
196
+ ```
197
+
198
+ ### Search by Status
199
+
200
+ ```bash
201
+ # Only subscribed
202
+ $ cakemail contacts list 123 --filter "status==subscribed"
203
+
204
+ # Unsubscribed contacts
205
+ $ cakemail contacts list 123 --filter "status==unsubscribed"
206
+
207
+ # Bounced contacts
208
+ $ cakemail contacts list 123 --filter "status==bounced"
209
+ ```
210
+
211
+ ### Search by Name
212
+
213
+ ```bash
214
+ $ cakemail contacts list 123 --filter "first_name==John"
215
+ $ cakemail contacts list 123 --filter "last_name==Doe"
216
+ ```
217
+
218
+ ### Search by Custom Attributes
219
+
220
+ ```bash
221
+ # Premium plan users
222
+ $ cakemail contacts list 123 --filter "custom_attributes.plan==premium"
223
+
224
+ # VIP contacts
225
+ $ cakemail contacts list 123 --filter "custom_attributes.is_vip==true"
226
+
227
+ # Recent signups
228
+ $ cakemail contacts list 123 --filter "custom_attributes.signup_date>=2024-03-01"
229
+ ```
230
+
231
+ ### Combine Filters
232
+
233
+ ```bash
234
+ # Premium VIP subscribers
235
+ $ cakemail contacts list 123 --filter "status==subscribed;custom_attributes.plan==premium;custom_attributes.is_vip==true"
236
+ ```
237
+
238
+ ### Pagination
239
+
240
+ ```bash
241
+ # First 100 contacts
242
+ $ cakemail contacts list 123 --limit 100
243
+
244
+ # Next 100 (page 2)
245
+ $ cakemail contacts list 123 --limit 100 --page 2
246
+
247
+ # Large list pagination
248
+ $ cakemail contacts list 123 --limit 500 --page 5
249
+ ```
250
+
251
+ ### Sort Results
252
+
253
+ ```bash
254
+ # Sort by email
255
+ $ cakemail contacts list 123 --sort "+email"
256
+
257
+ # Sort by subscription date (newest first)
258
+ $ cakemail contacts list 123 --sort "-subscribed_on"
259
+
260
+ # Sort by name
261
+ $ cakemail contacts list 123 --sort "+first_name,+last_name"
262
+ ```
263
+
264
+ ## Working with Custom Attributes
265
+
266
+ ### View Contact Attributes
267
+
268
+ ```bash
269
+ $ cakemail contacts get 123 501 -f json | jq '.custom_attributes'
270
+ ```
271
+
272
+ **Output:**
273
+ ```json
274
+ {
275
+ "plan": "premium",
276
+ "signup_date": "2024-03-15",
277
+ "lifetime_value": 299.99,
278
+ "is_vip": true,
279
+ "purchase_count": 5
280
+ }
281
+ ```
282
+
283
+ ### Add Attributes to Existing Contact
284
+
285
+ ```bash
286
+ $ cakemail contacts update 123 501 -d '{"company":"Acme Corp","industry":"Technology"}'
287
+ ```
288
+
289
+ ### Update Single Attribute
290
+
291
+ ```bash
292
+ # Upgrade plan
293
+ $ cakemail contacts update 123 501 -d '{"plan":"enterprise"}'
294
+
295
+ # Increment purchase count (requires fetching first)
296
+ CURRENT=$(cakemail contacts get 123 501 -f json | jq -r '.custom_attributes.purchase_count')
297
+ NEW=$((CURRENT + 1))
298
+ cakemail contacts update 123 501 -d "{\"purchase_count\":$NEW}"
299
+ ```
300
+
301
+ ### Remove Attribute
302
+
303
+ ```bash
304
+ # Set to null to remove
305
+ $ cakemail contacts update 123 501 -d '{"old_field":null}'
306
+ ```
307
+
308
+ ## Contact Workflows
309
+
310
+ ### Workflow 1: Add Contact with Complete Data
311
+
312
+ ```bash
313
+ #!/bin/bash
314
+ # add-complete-contact.sh
315
+
316
+ LIST_ID=123
317
+ EMAIL="$1"
318
+ FIRST_NAME="$2"
319
+ LAST_NAME="$3"
320
+ PLAN="$4"
321
+
322
+ if [ -z "$EMAIL" ] || [ -z "$FIRST_NAME" ] || [ -z "$LAST_NAME" ]; then
323
+ echo "Usage: $0 <email> <first-name> <last-name> [plan]"
324
+ exit 1
325
+ fi
326
+
327
+ PLAN=${PLAN:-basic}
328
+ SIGNUP_DATE=$(date +%Y-%m-%d)
329
+
330
+ echo "Adding contact: $FIRST_NAME $LAST_NAME ($EMAIL)"
331
+
332
+ CONTACT=$(cakemail contacts add $LIST_ID \
333
+ -e "$EMAIL" \
334
+ -f "$FIRST_NAME" \
335
+ -l "$LAST_NAME" \
336
+ -d "{
337
+ \"plan\": \"$PLAN\",
338
+ \"signup_date\": \"$SIGNUP_DATE\",
339
+ \"source\": \"manual_import\",
340
+ \"is_vip\": false,
341
+ \"purchase_count\": 0
342
+ }" \
343
+ -f json)
344
+
345
+ CONTACT_ID=$(echo "$CONTACT" | jq -r '.id')
346
+
347
+ echo "✓ Contact added: ID $CONTACT_ID"
348
+ echo " Email: $EMAIL"
349
+ echo " Name: $FIRST_NAME $LAST_NAME"
350
+ echo " Plan: $PLAN"
351
+ echo " Signup: $SIGNUP_DATE"
352
+ ```
353
+
354
+ **Usage:**
355
+ ```bash
356
+ $ ./add-complete-contact.sh john@example.com John Doe premium
357
+ ```
358
+
359
+ ### Workflow 2: Update Contact from Form Data
360
+
361
+ ```bash
362
+ #!/bin/bash
363
+ # update-from-form.sh
364
+
365
+ LIST_ID=123
366
+ EMAIL="$1"
367
+
368
+ if [ -z "$EMAIL" ]; then
369
+ echo "Usage: $0 <email>"
370
+ exit 1
371
+ fi
372
+
373
+ # Find contact by email
374
+ CONTACTS=$(cakemail contacts list $LIST_ID --filter "email==$EMAIL" -f json)
375
+ CONTACT_ID=$(echo "$CONTACTS" | jq -r '.data[0].id')
376
+
377
+ if [ "$CONTACT_ID" == "null" ]; then
378
+ echo "Contact not found: $EMAIL"
379
+ exit 1
380
+ fi
381
+
382
+ echo "Found contact: $CONTACT_ID"
383
+ echo ""
384
+
385
+ # Prompt for updates
386
+ read -p "First Name (Enter to skip): " FIRST_NAME
387
+ read -p "Last Name (Enter to skip): " LAST_NAME
388
+ read -p "Company (Enter to skip): " COMPANY
389
+ read -p "Plan (basic/premium/enterprise, Enter to skip): " PLAN
390
+
391
+ # Build update command
392
+ CMD="cakemail contacts update $LIST_ID $CONTACT_ID"
393
+
394
+ if [ -n "$FIRST_NAME" ]; then
395
+ CMD="$CMD -f \"$FIRST_NAME\""
396
+ fi
397
+
398
+ if [ -n "$LAST_NAME" ]; then
399
+ CMD="$CMD -l \"$LAST_NAME\""
400
+ fi
401
+
402
+ # Build custom attributes JSON
403
+ ATTRS="{"
404
+ if [ -n "$COMPANY" ]; then
405
+ ATTRS="$ATTRS\"company\":\"$COMPANY\","
406
+ fi
407
+ if [ -n "$PLAN" ]; then
408
+ ATTRS="$ATTRS\"plan\":\"$PLAN\","
409
+ fi
410
+ ATTRS="${ATTRS%,}}"
411
+
412
+ if [ "$ATTRS" != "{}" ]; then
413
+ CMD="$CMD -d '$ATTRS'"
414
+ fi
415
+
416
+ # Execute update
417
+ eval $CMD
418
+
419
+ echo ""
420
+ echo "✓ Contact updated"
421
+ ```
422
+
423
+ ### Workflow 3: Find and Update Multiple Contacts
424
+
425
+ ```bash
426
+ #!/bin/bash
427
+ # bulk-update-plan.sh
428
+
429
+ LIST_ID=123
430
+ OLD_PLAN="$1"
431
+ NEW_PLAN="$2"
432
+
433
+ if [ -z "$OLD_PLAN" ] || [ -z "$NEW_PLAN" ]; then
434
+ echo "Usage: $0 <old-plan> <new-plan>"
435
+ echo "Example: $0 basic premium"
436
+ exit 1
437
+ fi
438
+
439
+ echo "=== Bulk Plan Update ==="
440
+ echo "Updating contacts from $OLD_PLAN to $NEW_PLAN"
441
+ echo ""
442
+
443
+ # Find contacts with old plan
444
+ CONTACTS=$(cakemail contacts list $LIST_ID \
445
+ --filter "custom_attributes.plan==$OLD_PLAN" \
446
+ -f json | jq -r '.data[].id')
447
+
448
+ COUNT=$(echo "$CONTACTS" | wc -l)
449
+
450
+ if [ -z "$CONTACTS" ]; then
451
+ echo "No contacts found with plan: $OLD_PLAN"
452
+ exit 0
453
+ fi
454
+
455
+ echo "Found $COUNT contacts to update"
456
+ read -p "Continue? (yes/no): " CONFIRM
457
+
458
+ if [ "$CONFIRM" != "yes" ]; then
459
+ echo "Cancelled"
460
+ exit 0
461
+ fi
462
+
463
+ echo ""
464
+
465
+ # Update each contact
466
+ for CONTACT_ID in $CONTACTS; do
467
+ echo "Updating contact $CONTACT_ID..."
468
+ cakemail contacts update $LIST_ID $CONTACT_ID \
469
+ -d "{\"plan\":\"$NEW_PLAN\"}"
470
+ done
471
+
472
+ echo ""
473
+ echo "✓ Updated $COUNT contacts"
474
+ ```
475
+
476
+ ### Workflow 4: Contact Activity Report
477
+
478
+ ```bash
479
+ #!/bin/bash
480
+ # contact-activity.sh
481
+
482
+ LIST_ID=123
483
+ CONTACT_ID=$1
484
+
485
+ if [ -z "$CONTACT_ID" ]; then
486
+ echo "Usage: $0 <contact-id>"
487
+ exit 1
488
+ fi
489
+
490
+ echo "=== Contact Activity Report ==="
491
+ echo ""
492
+
493
+ # Get contact details
494
+ CONTACT=$(cakemail contacts get $LIST_ID $CONTACT_ID -f json)
495
+
496
+ echo "Contact: $(echo "$CONTACT" | jq -r '.first_name') $(echo "$CONTACT" | jq -r '.last_name')"
497
+ echo "Email: $(echo "$CONTACT" | jq -r '.email')"
498
+ echo "Status: $(echo "$CONTACT" | jq -r '.status')"
499
+ echo ""
500
+
501
+ # Subscription info
502
+ echo "=== Subscription ==="
503
+ echo "Subscribed: $(echo "$CONTACT" | jq -r '.subscribed_on')"
504
+ echo "Last Modified: $(echo "$CONTACT" | jq -r '.last_modified')"
505
+ echo ""
506
+
507
+ # Engagement
508
+ echo "=== Engagement ==="
509
+ echo "Last Open: $(echo "$CONTACT" | jq -r '.last_open_date // "Never"')"
510
+ echo "Last Click: $(echo "$CONTACT" | jq -r '.last_click_date // "Never"')"
511
+ echo "Total Opens: $(echo "$CONTACT" | jq -r '.total_opens // 0')"
512
+ echo "Total Clicks: $(echo "$CONTACT" | jq -r '.total_clicks // 0')"
513
+ echo ""
514
+
515
+ # Bounces
516
+ BOUNCES=$(echo "$CONTACT" | jq -r '.bounces_count')
517
+ echo "=== Deliverability ==="
518
+ echo "Bounce Count: $BOUNCES"
519
+ if [ $BOUNCES -gt 0 ]; then
520
+ echo "Last Bounce: $(echo "$CONTACT" | jq -r '.last_bounce')"
521
+ fi
522
+ echo ""
523
+
524
+ # Custom attributes
525
+ echo "=== Custom Attributes ==="
526
+ echo "$CONTACT" | jq '.custom_attributes'
527
+ ```
528
+
529
+ ### Workflow 5: Contact Enrichment
530
+
531
+ ```bash
532
+ #!/bin/bash
533
+ # enrich-contact.sh
534
+
535
+ LIST_ID=123
536
+ EMAIL="$1"
537
+
538
+ if [ -z "$EMAIL" ]; then
539
+ echo "Usage: $0 <email>"
540
+ exit 1
541
+ fi
542
+
543
+ echo "=== Contact Enrichment ==="
544
+ echo ""
545
+
546
+ # Find contact
547
+ CONTACTS=$(cakemail contacts list $LIST_ID --filter "email==$EMAIL" -f json)
548
+ CONTACT_ID=$(echo "$CONTACTS" | jq -r '.data[0].id')
549
+
550
+ if [ "$CONTACT_ID" == "null" ]; then
551
+ echo "Contact not found: $EMAIL"
552
+ exit 1
553
+ fi
554
+
555
+ echo "Found contact: $CONTACT_ID"
556
+ echo ""
557
+
558
+ # Get current data
559
+ CONTACT=$(cakemail contacts get $LIST_ID $CONTACT_ID -f json)
560
+
561
+ echo "Current data:"
562
+ echo "$CONTACT" | jq '{email, first_name, last_name, custom_attributes}'
563
+ echo ""
564
+
565
+ # Enrich from external source (example: your CRM)
566
+ echo "Fetching enrichment data..."
567
+
568
+ # Example: Call your API or database
569
+ # ENRICHMENT=$(curl -s "https://api.yourcrm.com/contact?email=$EMAIL")
570
+
571
+ # Mock enrichment data
572
+ ENRICHMENT='{
573
+ "company": "Acme Corp",
574
+ "title": "Marketing Manager",
575
+ "industry": "Technology",
576
+ "company_size": "50-100",
577
+ "linkedin_url": "https://linkedin.com/in/johndoe"
578
+ }'
579
+
580
+ echo "Enrichment data:"
581
+ echo "$ENRICHMENT" | jq '.'
582
+ echo ""
583
+
584
+ read -p "Apply enrichment? (yes/no): " APPLY
585
+
586
+ if [ "$APPLY" == "yes" ]; then
587
+ # Merge with existing custom attributes
588
+ EXISTING=$(echo "$CONTACT" | jq '.custom_attributes')
589
+ MERGED=$(echo "$EXISTING $ENRICHMENT" | jq -s '.[0] + .[1]')
590
+
591
+ # Update contact
592
+ cakemail contacts update $LIST_ID $CONTACT_ID -d "$MERGED"
593
+
594
+ echo ""
595
+ echo "✓ Contact enriched"
596
+ fi
597
+ ```
598
+
599
+ ## Contact Status Management
600
+
601
+ ### Understanding Contact Status
602
+
603
+ **Status values:**
604
+ - `subscribed` - Active, can receive emails
605
+ - `unsubscribed` - Opted out, cannot send
606
+ - `bounced` - Email bounced, delivery failed
607
+ - `pending` - Awaiting confirmation (double opt-in)
608
+
609
+ ### Resubscribe Contact
610
+
611
+ ```bash
612
+ # Change unsubscribed back to subscribed
613
+ $ cakemail contacts update 123 501 --status subscribed
614
+ ```
615
+
616
+ **Note:** Only do this with explicit consent!
617
+
618
+ ### Handle Bounced Contacts
619
+
620
+ ```bash
621
+ # Find bounced contacts
622
+ $ cakemail contacts list 123 --filter "status==bounced"
623
+
624
+ # If email corrected, mark as subscribed
625
+ $ cakemail contacts update 123 501 --status subscribed
626
+
627
+ # Or delete if permanently invalid
628
+ $ cakemail contacts delete 123 501 --force
629
+ ```
630
+
631
+ ### Find Pending Confirmations
632
+
633
+ ```bash
634
+ # Double opt-in pending
635
+ $ cakemail contacts list 123 --filter "status==pending"
636
+
637
+ # Resend confirmation (if supported)
638
+ # Or manually approve
639
+ $ cakemail contacts update 123 501 --status subscribed
640
+ ```
641
+
642
+ ## Contact Validation
643
+
644
+ ### Validate Email Format
645
+
646
+ ```bash
647
+ #!/bin/bash
648
+ # validate-email.sh
649
+
650
+ EMAIL="$1"
651
+
652
+ if [[ ! "$EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
653
+ echo "❌ Invalid email format: $EMAIL"
654
+ exit 1
655
+ fi
656
+
657
+ echo "✅ Valid email format: $EMAIL"
658
+ ```
659
+
660
+ ### Validate Before Adding
661
+
662
+ ```bash
663
+ #!/bin/bash
664
+ # safe-add-contact.sh
665
+
666
+ LIST_ID=123
667
+ EMAIL="$1"
668
+ FIRST_NAME="$2"
669
+ LAST_NAME="$3"
670
+
671
+ # Validate email
672
+ if [[ ! "$EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
673
+ echo "❌ Invalid email: $EMAIL"
674
+ exit 1
675
+ fi
676
+
677
+ # Check if already exists
678
+ EXISTING=$(cakemail contacts list $LIST_ID --filter "email==$EMAIL" -f json | jq '.count')
679
+
680
+ if [ $EXISTING -gt 0 ]; then
681
+ echo "⚠️ Contact already exists: $EMAIL"
682
+ read -p "Update instead? (yes/no): " UPDATE
683
+
684
+ if [ "$UPDATE" == "yes" ]; then
685
+ CONTACT_ID=$(cakemail contacts list $LIST_ID --filter "email==$EMAIL" -f json | jq -r '.data[0].id')
686
+ cakemail contacts update $LIST_ID $CONTACT_ID -f "$FIRST_NAME" -l "$LAST_NAME"
687
+ echo "✓ Contact updated"
688
+ fi
689
+ exit 0
690
+ fi
691
+
692
+ # Add contact
693
+ cakemail contacts add $LIST_ID -e "$EMAIL" -f "$FIRST_NAME" -l "$LAST_NAME"
694
+ echo "✓ Contact added"
695
+ ```
696
+
697
+ ## Contact Deduplication
698
+
699
+ ### Find Duplicate Emails
700
+
701
+ ```bash
702
+ #!/bin/bash
703
+ # find-duplicates.sh
704
+
705
+ LIST_ID=123
706
+
707
+ echo "=== Finding Duplicate Contacts ==="
708
+ echo ""
709
+
710
+ # Export all contacts
711
+ cakemail contacts export $LIST_ID
712
+ EXPORT_ID=$(cakemail contacts export-list -f json | jq -r '.data[0].id')
713
+
714
+ # Wait for export
715
+ sleep 10
716
+
717
+ # Download and analyze
718
+ cakemail contacts export-download $EXPORT_ID > contacts.csv
719
+
720
+ # Find duplicates
721
+ echo "Duplicate emails:"
722
+ cut -d',' -f1 contacts.csv | sort | uniq -d
723
+
724
+ echo ""
725
+ echo "Run manual review to keep best version of each"
726
+
727
+ rm contacts.csv
728
+ ```
729
+
730
+ ### Remove Duplicates (Keep First)
731
+
732
+ ```bash
733
+ #!/bin/bash
734
+ # remove-duplicates.sh
735
+
736
+ LIST_ID=123
737
+ EMAIL="$1"
738
+
739
+ if [ -z "$EMAIL" ]; then
740
+ echo "Usage: $0 <duplicate-email>"
741
+ exit 1
742
+ fi
743
+
744
+ # Find all instances
745
+ CONTACTS=$(cakemail contacts list $LIST_ID --filter "email==$EMAIL" -f json)
746
+ COUNT=$(echo "$CONTACTS" | jq '.count')
747
+
748
+ if [ $COUNT -le 1 ]; then
749
+ echo "No duplicates found for: $EMAIL"
750
+ exit 0
751
+ fi
752
+
753
+ echo "Found $COUNT instances of $EMAIL"
754
+ echo ""
755
+
756
+ # Show all instances
757
+ echo "$CONTACTS" | jq -r '.data[] | "ID: \(.id) - Subscribed: \(.subscribed_on)"'
758
+ echo ""
759
+
760
+ # Keep first, delete rest
761
+ KEEP=$(echo "$CONTACTS" | jq -r '.data[0].id')
762
+ TO_DELETE=$(echo "$CONTACTS" | jq -r '.data[1:][].id')
763
+
764
+ echo "Keeping contact: $KEEP"
765
+ echo "Deleting: $TO_DELETE"
766
+ echo ""
767
+
768
+ read -p "Proceed? (yes/no): " CONFIRM
769
+
770
+ if [ "$CONFIRM" == "yes" ]; then
771
+ for ID in $TO_DELETE; do
772
+ cakemail contacts delete $LIST_ID $ID --force
773
+ echo " Deleted: $ID"
774
+ done
775
+ echo "✓ Deduplication complete"
776
+ fi
777
+ ```
778
+
779
+ ## Contact Segmentation
780
+
781
+ ### Create Targeted Lists from Filters
782
+
783
+ ```bash
784
+ #!/bin/bash
785
+ # extract-segment.sh
786
+
787
+ SOURCE_LIST=123
788
+ TARGET_LIST=124
789
+ FILTER="custom_attributes.plan==premium"
790
+
791
+ echo "=== Extracting Segment ==="
792
+ echo "Source: $SOURCE_LIST"
793
+ echo "Target: $TARGET_LIST"
794
+ echo "Filter: $FILTER"
795
+ echo ""
796
+
797
+ # Get filtered contacts
798
+ CONTACTS=$(cakemail contacts list $SOURCE_LIST --filter "$FILTER" -f json | jq -r '.data[].email')
799
+
800
+ COUNT=$(echo "$CONTACTS" | wc -l)
801
+ echo "Found $COUNT contacts matching filter"
802
+ echo ""
803
+
804
+ read -p "Copy to target list? (yes/no): " CONFIRM
805
+
806
+ if [ "$CONFIRM" == "yes" ]; then
807
+ for EMAIL in $CONTACTS; do
808
+ # Get full contact data
809
+ CONTACT=$(cakemail contacts list $SOURCE_LIST --filter "email==$EMAIL" -f json | jq '.data[0]')
810
+
811
+ FIRST_NAME=$(echo "$CONTACT" | jq -r '.first_name')
812
+ LAST_NAME=$(echo "$CONTACT" | jq -r '.last_name')
813
+ CUSTOM_ATTRS=$(echo "$CONTACT" | jq -c '.custom_attributes')
814
+
815
+ # Add to target list
816
+ cakemail contacts add $TARGET_LIST \
817
+ -e "$EMAIL" \
818
+ -f "$FIRST_NAME" \
819
+ -l "$LAST_NAME" \
820
+ -d "$CUSTOM_ATTRS" 2>/dev/null
821
+
822
+ echo " Copied: $EMAIL"
823
+ done
824
+
825
+ echo ""
826
+ echo "✓ Segment extracted"
827
+ fi
828
+ ```
829
+
830
+ ## Contact Analytics
831
+
832
+ ### Engagement Scoring
833
+
834
+ ```bash
835
+ #!/bin/bash
836
+ # engagement-score.sh
837
+
838
+ LIST_ID=123
839
+ CONTACT_ID=$1
840
+
841
+ if [ -z "$CONTACT_ID" ]; then
842
+ echo "Usage: $0 <contact-id>"
843
+ exit 1
844
+ fi
845
+
846
+ CONTACT=$(cakemail contacts get $LIST_ID $CONTACT_ID -f json)
847
+
848
+ # Get metrics
849
+ TOTAL_OPENS=$(echo "$CONTACT" | jq -r '.total_opens // 0')
850
+ TOTAL_CLICKS=$(echo "$CONTACT" | jq -r '.total_clicks // 0')
851
+ LAST_OPEN=$(echo "$CONTACT" | jq -r '.last_open_date // "null"')
852
+
853
+ # Calculate score (0-100)
854
+ SCORE=0
855
+
856
+ # Opens contribute 30 points max
857
+ if [ $TOTAL_OPENS -gt 0 ]; then
858
+ OPEN_SCORE=$((TOTAL_OPENS > 30 ? 30 : TOTAL_OPENS))
859
+ SCORE=$((SCORE + OPEN_SCORE))
860
+ fi
861
+
862
+ # Clicks contribute 40 points max
863
+ if [ $TOTAL_CLICKS -gt 0 ]; then
864
+ CLICK_SCORE=$((TOTAL_CLICKS * 4))
865
+ CLICK_SCORE=$((CLICK_SCORE > 40 ? 40 : CLICK_SCORE))
866
+ SCORE=$((SCORE + CLICK_SCORE))
867
+ fi
868
+
869
+ # Recent activity contributes 30 points
870
+ if [ "$LAST_OPEN" != "null" ]; then
871
+ DAYS_AGO=$(( ($(date +%s) - $(date -d "$LAST_OPEN" +%s)) / 86400 ))
872
+
873
+ if [ $DAYS_AGO -lt 7 ]; then
874
+ SCORE=$((SCORE + 30))
875
+ elif [ $DAYS_AGO -lt 30 ]; then
876
+ SCORE=$((SCORE + 20))
877
+ elif [ $DAYS_AGO -lt 90 ]; then
878
+ SCORE=$((SCORE + 10))
879
+ fi
880
+ fi
881
+
882
+ echo "=== Engagement Score ==="
883
+ echo "Contact: $(echo "$CONTACT" | jq -r '.email')"
884
+ echo "Total Opens: $TOTAL_OPENS"
885
+ echo "Total Clicks: $TOTAL_CLICKS"
886
+ echo "Last Open: ${LAST_OPEN:-Never}"
887
+ echo ""
888
+ echo "Engagement Score: $SCORE/100"
889
+ echo ""
890
+
891
+ if [ $SCORE -ge 70 ]; then
892
+ echo "🟢 Highly Engaged"
893
+ elif [ $SCORE -ge 40 ]; then
894
+ echo "🟡 Moderately Engaged"
895
+ elif [ $SCORE -ge 20 ]; then
896
+ echo "🟠 Low Engagement"
897
+ else
898
+ echo "🔴 At Risk - Very Low Engagement"
899
+ fi
900
+ ```
901
+
902
+ ### Contact Lifetime Value
903
+
904
+ ```bash
905
+ #!/bin/bash
906
+ # calculate-ltv.sh
907
+
908
+ LIST_ID=123
909
+
910
+ echo "=== Contact Lifetime Value Analysis ==="
911
+ echo ""
912
+
913
+ # Get all contacts with LTV data
914
+ CONTACTS=$(cakemail contacts list $LIST_ID \
915
+ --filter "status==subscribed" \
916
+ -f json | jq -r '.data[] | "\(.id),\(.email),\(.custom_attributes.lifetime_value // 0)"')
917
+
918
+ TOTAL_LTV=0
919
+ COUNT=0
920
+
921
+ echo "Contact ID | Email | LTV"
922
+ echo "-----------|-------|-----"
923
+
924
+ while IFS=',' read ID EMAIL LTV; do
925
+ printf "%-10s | %-25s | $%7.2f\n" "$ID" "$EMAIL" "$LTV"
926
+ TOTAL_LTV=$(echo "$TOTAL_LTV + $LTV" | bc)
927
+ COUNT=$((COUNT + 1))
928
+ done <<< "$CONTACTS"
929
+
930
+ echo ""
931
+ echo "Total Contacts: $COUNT"
932
+ echo "Total LTV: \$$(echo "scale=2; $TOTAL_LTV" | bc)"
933
+ echo "Average LTV: \$$(echo "scale=2; $TOTAL_LTV / $COUNT" | bc)"
934
+ ```
935
+
936
+ ## Troubleshooting
937
+
938
+ ### Cannot Add Contact - Email Already Exists
939
+
940
+ **Error:** "Contact with this email already exists"
941
+
942
+ **Solution:**
943
+ ```bash
944
+ # Find existing contact
945
+ $ cakemail contacts list 123 --filter "email==john@example.com" -f json
946
+
947
+ # Update instead of add
948
+ CONTACT_ID=$(cakemail contacts list 123 --filter "email==john@example.com" -f json | jq -r '.data[0].id')
949
+ $ cakemail contacts update 123 $CONTACT_ID -f "John" -l "Doe"
950
+ ```
951
+
952
+ ### Contact Not Found
953
+
954
+ **Error:** "Contact ID not found"
955
+
956
+ **Solution:**
957
+ ```bash
958
+ # Verify contact ID
959
+ $ cakemail contacts list 123 --filter "email==john@example.com"
960
+
961
+ # Ensure correct list ID
962
+ $ cakemail lists list
963
+
964
+ # Contact may have been deleted
965
+ # Search across all lists if needed
966
+ ```
967
+
968
+ ### Custom Attribute Not Saving
969
+
970
+ **Problem:** Custom attributes not appearing
971
+
972
+ **Solution:**
973
+ ```bash
974
+ # Ensure attribute exists
975
+ $ cakemail attributes list 123
976
+
977
+ # Create attribute first
978
+ $ cakemail attributes create 123 -n "plan" -t "text"
979
+
980
+ # Then add contact with attribute
981
+ $ cakemail contacts add 123 -e "user@example.com" -d '{"plan":"premium"}'
982
+
983
+ # Verify
984
+ $ cakemail contacts get 123 <contact-id> -f json | jq '.custom_attributes'
985
+ ```
986
+
987
+ ### Invalid Data Format
988
+
989
+ **Error:** "Invalid JSON format for custom attributes"
990
+
991
+ **Solution:**
992
+ ```bash
993
+ # ❌ Wrong: Single quotes for JSON
994
+ $ cakemail contacts add 123 -e "user@example.com" -d '{plan:premium}'
995
+
996
+ # ✅ Correct: Proper JSON with double quotes
997
+ $ cakemail contacts add 123 -e "user@example.com" -d '{"plan":"premium"}'
998
+
999
+ # ✅ Correct: Escaped in shell
1000
+ $ cakemail contacts add 123 -e "user@example.com" -d "{\"plan\":\"premium\"}"
1001
+ ```
1002
+
1003
+ ### Bulk Operations Too Slow
1004
+
1005
+ **Problem:** Adding many contacts individually is slow
1006
+
1007
+ **Solution:**
1008
+ ```bash
1009
+ # Use import instead of individual adds
1010
+ # See contact-import-export.md guide
1011
+
1012
+ # Create CSV file
1013
+ echo "email,first_name,last_name,plan" > contacts.csv
1014
+ echo "user1@example.com,John,Doe,premium" >> contacts.csv
1015
+ echo "user2@example.com,Jane,Smith,basic" >> contacts.csv
1016
+
1017
+ # Import in bulk
1018
+ $ cakemail contacts import 123 --file contacts.csv
1019
+
1020
+ # Much faster than individual adds
1021
+ ```
1022
+
1023
+ ## Best Practices Summary
1024
+
1025
+ 1. **Validate before adding** - Check email format and duplicates
1026
+ 2. **Use custom attributes** - Store business-specific data
1027
+ 3. **Keep data current** - Update contacts regularly
1028
+ 4. **Track consent** - Document subscription source and date
1029
+ 5. **Monitor engagement** - Identify and re-engage inactive contacts
1030
+ 6. **Clean regularly** - Remove bounced and invalid contacts
1031
+ 7. **Bulk operations** - Use import for adding many contacts
1032
+ 8. **Segment intelligently** - Use attributes for targeting
1033
+ 9. **Document schema** - Keep track of custom attribute meanings
1034
+ 10. **Respect unsubscribes** - Never resubscribe without explicit consent
1035
+