@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,932 @@
1
+ # Bulk Operations & Data Processing
2
+
3
+ Efficiently process large datasets and perform bulk operations with the Cakemail CLI.
4
+
5
+ ## Overview
6
+
7
+ Learn to:
8
+ - Import/export large contact lists
9
+ - Process thousands of records efficiently
10
+ - Manage bulk campaign operations
11
+ - Handle memory constraints
12
+ - Track progress for long operations
13
+ - Recover from failures
14
+
15
+ ## Bulk Contact Import
16
+
17
+ ### Large CSV Import Strategy
18
+
19
+ ```bash
20
+ #!/bin/bash
21
+
22
+ # Import 100,000+ contacts efficiently
23
+ import_large_list() {
24
+ local list_id="$1"
25
+ local csv_file="$2"
26
+ local chunk_size=1000
27
+ local temp_dir="import-chunks"
28
+
29
+ mkdir -p "$temp_dir"
30
+
31
+ # Count total contacts
32
+ local total=$(tail -n +2 "$csv_file" | wc -l)
33
+ echo "Importing $total contacts in chunks of $chunk_size..."
34
+
35
+ # Split CSV into chunks
36
+ tail -n +2 "$csv_file" | split -l $chunk_size - "$temp_dir/chunk-"
37
+
38
+ # Add header to each chunk
39
+ local header=$(head -1 "$csv_file")
40
+ for chunk in "$temp_dir"/chunk-*; do
41
+ sed -i.bak "1i\\
42
+ $header" "$chunk"
43
+ rm "${chunk}.bak"
44
+ done
45
+
46
+ # Import each chunk
47
+ local count=0
48
+ for chunk in "$temp_dir"/chunk-*; do
49
+ echo "Importing chunk $(basename "$chunk")..."
50
+
51
+ if cakemail contacts import "$list_id" --file "$chunk"; then
52
+ ((count += chunk_size))
53
+ local percent=$((count * 100 / total))
54
+ echo "Progress: $count/$total ($percent%)"
55
+ else
56
+ echo "ERROR: Failed to import $chunk"
57
+ echo "Resume from: $chunk"
58
+ exit 1
59
+ fi
60
+
61
+ # Rate limiting
62
+ sleep 2
63
+ done
64
+
65
+ # Cleanup
66
+ rm -rf "$temp_dir"
67
+
68
+ echo "Import complete: $total contacts"
69
+ }
70
+
71
+ # Usage
72
+ import_large_list 123 "contacts-100k.csv"
73
+ ```
74
+
75
+ ### Parallel Import with Job Control
76
+
77
+ ```bash
78
+ #!/bin/bash
79
+
80
+ # Import multiple lists in parallel
81
+ parallel_import() {
82
+ local max_jobs=3
83
+ local -a pids=()
84
+
85
+ # List of imports to perform
86
+ declare -A imports=(
87
+ [123]="list-a-contacts.csv"
88
+ [124]="list-b-contacts.csv"
89
+ [125]="list-c-contacts.csv"
90
+ [126]="list-d-contacts.csv"
91
+ )
92
+
93
+ for list_id in "${!imports[@]}"; do
94
+ # Wait if max jobs reached
95
+ while [ ${#pids[@]} -ge $max_jobs ]; do
96
+ # Check for completed jobs
97
+ for i in "${!pids[@]}"; do
98
+ if ! kill -0 "${pids[$i]}" 2>/dev/null; then
99
+ unset 'pids[$i]'
100
+ fi
101
+ done
102
+ pids=("${pids[@]}") # Reindex array
103
+ sleep 1
104
+ done
105
+
106
+ # Start import in background
107
+ (
108
+ csv_file="${imports[$list_id]}"
109
+ echo "Starting import for list $list_id from $csv_file"
110
+
111
+ if cakemail contacts import "$list_id" --file "$csv_file"; then
112
+ echo "✓ List $list_id import complete"
113
+ else
114
+ echo "✗ List $list_id import failed"
115
+ fi
116
+ ) &
117
+
118
+ pids+=($!)
119
+ echo "Started job ${pids[-1]} for list $list_id"
120
+ done
121
+
122
+ # Wait for all jobs to complete
123
+ echo "Waiting for ${#pids[@]} jobs to complete..."
124
+ for pid in "${pids[@]}"; do
125
+ wait "$pid"
126
+ done
127
+
128
+ echo "All imports complete"
129
+ }
130
+
131
+ parallel_import
132
+ ```
133
+
134
+ ### Resumable Import with Checkpoints
135
+
136
+ ```bash
137
+ #!/bin/bash
138
+
139
+ # Resume import from checkpoint after failure
140
+ resumable_import() {
141
+ local list_id="$1"
142
+ local csv_file="$2"
143
+ local checkpoint_file=".import-checkpoint-${list_id}"
144
+ local start_line=1
145
+
146
+ # Check for checkpoint
147
+ if [ -f "$checkpoint_file" ]; then
148
+ start_line=$(cat "$checkpoint_file")
149
+ echo "Resuming from line $start_line"
150
+ fi
151
+
152
+ # Count total lines
153
+ local total=$(wc -l < "$csv_file")
154
+
155
+ # Process line by line
156
+ local line_num=0
157
+ local imported=0
158
+
159
+ while IFS=, read -r email first_name last_name custom_data; do
160
+ ((line_num++))
161
+
162
+ # Skip until start_line
163
+ [ $line_num -lt $start_line ] && continue
164
+
165
+ # Skip header
166
+ [ $line_num -eq 1 ] && continue
167
+
168
+ # Import contact
169
+ if cakemail contacts add "$list_id" \
170
+ -e "$email" \
171
+ -f "$first_name" \
172
+ -l "$last_name" \
173
+ ${custom_data:+-d "$custom_data"} 2>/dev/null; then
174
+ ((imported++))
175
+ else
176
+ echo "Warning: Failed to import $email (line $line_num)"
177
+ fi
178
+
179
+ # Update checkpoint every 100 contacts
180
+ if [ $((imported % 100)) -eq 0 ]; then
181
+ echo "$line_num" > "$checkpoint_file"
182
+ local percent=$((line_num * 100 / total))
183
+ echo "Progress: $line_num/$total ($percent%) - $imported imported"
184
+ fi
185
+
186
+ # Rate limiting
187
+ [ $((imported % 10)) -eq 0 ] && sleep 1
188
+ done < "$csv_file"
189
+
190
+ # Remove checkpoint on success
191
+ rm -f "$checkpoint_file"
192
+
193
+ echo "Import complete: $imported contacts from $total lines"
194
+ }
195
+
196
+ # Usage - will resume if interrupted
197
+ resumable_import 123 "contacts.csv"
198
+ ```
199
+
200
+ ## Bulk Contact Export
201
+
202
+ ### Export All Lists
203
+
204
+ ```bash
205
+ #!/bin/bash
206
+
207
+ # Export all lists with timestamped backups
208
+ export_all_lists() {
209
+ local backup_dir="backups/$(date +%Y%m%d-%H%M%S)"
210
+ mkdir -p "$backup_dir"
211
+
212
+ echo "Exporting all lists to $backup_dir"
213
+
214
+ # Get all lists
215
+ local lists=$(cakemail lists list -f json | jq -r '.data[].id')
216
+ local total=$(echo "$lists" | wc -w)
217
+ local count=0
218
+
219
+ for list_id in $lists; do
220
+ ((count++))
221
+
222
+ # Get list name for filename
223
+ local list_name=$(cakemail lists get "$list_id" -f json | \
224
+ jq -r '.name' | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
225
+
226
+ local filename="${backup_dir}/list-${list_id}-${list_name}.csv"
227
+
228
+ echo "[$count/$total] Exporting list $list_id: $list_name"
229
+
230
+ # Export contacts
231
+ if cakemail contacts export "$list_id" -f json > "${filename}.meta"; then
232
+ local export_id=$(jq -r '.export_id' "${filename}.meta")
233
+
234
+ # Wait for export to complete
235
+ while true; do
236
+ local status=$(cakemail contacts export-status "$export_id" -f json | \
237
+ jq -r '.status')
238
+
239
+ if [ "$status" = "completed" ]; then
240
+ break
241
+ elif [ "$status" = "failed" ]; then
242
+ echo "ERROR: Export failed for list $list_id"
243
+ continue 2
244
+ fi
245
+
246
+ sleep 2
247
+ done
248
+
249
+ # Download export
250
+ cakemail contacts export-download "$export_id" > "$filename"
251
+ echo "✓ Exported $(wc -l < "$filename") contacts to $filename"
252
+
253
+ rm "${filename}.meta"
254
+ else
255
+ echo "✗ Failed to start export for list $list_id"
256
+ fi
257
+
258
+ # Rate limiting
259
+ sleep 1
260
+ done
261
+
262
+ # Create archive
263
+ echo "Creating archive..."
264
+ tar -czf "${backup_dir}.tar.gz" -C "$(dirname "$backup_dir")" \
265
+ "$(basename "$backup_dir")"
266
+
267
+ echo "Backup complete: ${backup_dir}.tar.gz"
268
+ }
269
+
270
+ export_all_lists
271
+ ```
272
+
273
+ ### Incremental Export (Changed Contacts Only)
274
+
275
+ ```bash
276
+ #!/bin/bash
277
+
278
+ # Export only contacts modified since last export
279
+ incremental_export() {
280
+ local list_id="$1"
281
+ local state_file=".last-export-${list_id}"
282
+ local output_file="incremental-export-$(date +%Y%m%d).csv"
283
+
284
+ # Get last export timestamp
285
+ local last_export=$([ -f "$state_file" ] && cat "$state_file" || echo "2000-01-01")
286
+
287
+ echo "Exporting contacts modified since: $last_export"
288
+
289
+ # Export with filter
290
+ cakemail contacts list "$list_id" \
291
+ --filter "updated_at>=$last_export" \
292
+ -f json | \
293
+ jq -r '["email","first_name","last_name","status","updated_at"],
294
+ (.data[] | [.email, .first_name, .last_name, .status, .updated_at]) |
295
+ @csv' > "$output_file"
296
+
297
+ # Count exported contacts
298
+ local count=$(($(wc -l < "$output_file") - 1))
299
+
300
+ if [ $count -gt 0 ]; then
301
+ echo "Exported $count changed contacts to $output_file"
302
+
303
+ # Update state
304
+ date -Iseconds > "$state_file"
305
+ else
306
+ echo "No changes since last export"
307
+ rm "$output_file"
308
+ fi
309
+ }
310
+
311
+ # Usage
312
+ incremental_export 123
313
+ ```
314
+
315
+ ## Bulk Campaign Operations
316
+
317
+ ### Create Multiple Campaigns from Templates
318
+
319
+ ```bash
320
+ #!/bin/bash
321
+
322
+ # Create campaigns for multiple segments
323
+ create_segmented_campaigns() {
324
+ local list_id="$1"
325
+ local sender_id="$2"
326
+ local template_id="$3"
327
+ local base_name="$4"
328
+
329
+ # Get all segments
330
+ local segments=$(cakemail segments list "$list_id" -f json | \
331
+ jq -r '.data[] | "\(.id):\(.name)"')
332
+
333
+ echo "Creating campaigns for each segment..."
334
+
335
+ while IFS=: read -r segment_id segment_name; do
336
+ echo "Creating campaign for segment: $segment_name"
337
+
338
+ # Create campaign
339
+ campaign_id=$(cakemail campaigns create \
340
+ -n "${base_name} - ${segment_name}" \
341
+ -l "$list_id" \
342
+ -s "$sender_id" \
343
+ --template "$template_id" \
344
+ --segment "$segment_id" \
345
+ --subject "Personalized for ${segment_name}" \
346
+ -f json | jq -r '.id')
347
+
348
+ if [ -n "$campaign_id" ]; then
349
+ echo "✓ Created campaign $campaign_id for segment $segment_name"
350
+ else
351
+ echo "✗ Failed to create campaign for segment $segment_name"
352
+ fi
353
+
354
+ sleep 1
355
+ done <<< "$segments"
356
+
357
+ echo "Campaign creation complete"
358
+ }
359
+
360
+ # Usage
361
+ create_segmented_campaigns 123 101 201 "Weekly Newsletter"
362
+ ```
363
+
364
+ ### Bulk Campaign Scheduling
365
+
366
+ ```bash
367
+ #!/bin/bash
368
+
369
+ # Schedule multiple campaigns with staggered send times
370
+ bulk_schedule_campaigns() {
371
+ local -a campaign_ids=("$@")
372
+ local start_date="2024-03-20"
373
+ local start_time="08:00:00"
374
+ local interval_hours=24
375
+
376
+ local current_timestamp=$(date -d "$start_date $start_time" +%s)
377
+
378
+ for campaign_id in "${campaign_ids[@]}"; do
379
+ # Calculate send time
380
+ local send_datetime=$(date -d "@$current_timestamp" "+%Y-%m-%d %H:%M:%S")
381
+
382
+ echo "Scheduling campaign $campaign_id for $send_datetime"
383
+
384
+ if cakemail campaigns schedule "$campaign_id" --when "$send_datetime"; then
385
+ echo "✓ Scheduled campaign $campaign_id"
386
+ else
387
+ echo "✗ Failed to schedule campaign $campaign_id"
388
+ fi
389
+
390
+ # Increment timestamp
391
+ current_timestamp=$((current_timestamp + interval_hours * 3600))
392
+
393
+ sleep 1
394
+ done
395
+ }
396
+
397
+ # Schedule campaigns 24 hours apart
398
+ bulk_schedule_campaigns 790 791 792 793 794
399
+ ```
400
+
401
+ ### Archive Old Campaigns
402
+
403
+ ```bash
404
+ #!/bin/bash
405
+
406
+ # Archive campaigns older than N days
407
+ archive_old_campaigns() {
408
+ local days_old="${1:-90}"
409
+ local cutoff_date=$(date -d "$days_old days ago" +%Y-%m-%d)
410
+
411
+ echo "Archiving campaigns older than $cutoff_date..."
412
+
413
+ # Get old campaigns
414
+ local campaigns=$(cakemail campaigns list \
415
+ --filter "sent_at<$cutoff_date;status==sent" \
416
+ -f json | jq -r '.data[].id')
417
+
418
+ local count=0
419
+
420
+ for campaign_id in $campaigns; do
421
+ echo "Archiving campaign $campaign_id"
422
+
423
+ if cakemail campaigns archive "$campaign_id"; then
424
+ ((count++))
425
+ echo "✓ Archived campaign $campaign_id"
426
+ else
427
+ echo "✗ Failed to archive campaign $campaign_id"
428
+ fi
429
+
430
+ sleep 0.5
431
+ done
432
+
433
+ echo "Archived $count campaigns"
434
+ }
435
+
436
+ # Archive campaigns older than 90 days
437
+ archive_old_campaigns 90
438
+ ```
439
+
440
+ ## Bulk Report Generation
441
+
442
+ ### Generate Reports for All Recent Campaigns
443
+
444
+ ```bash
445
+ #!/bin/bash
446
+
447
+ # Generate and save reports for all campaigns from last 30 days
448
+ bulk_report_generation() {
449
+ local days="${1:-30}"
450
+ local output_dir="reports/$(date +%Y%m%d)"
451
+ local start_date=$(date -d "$days days ago" +%Y-%m-%d)
452
+
453
+ mkdir -p "$output_dir"
454
+
455
+ echo "Generating reports for campaigns since $start_date"
456
+
457
+ # Get campaigns
458
+ local campaigns=$(cakemail campaigns list \
459
+ --filter "sent_at>=$start_date;status==sent" \
460
+ -f json | jq -r '.data[] | "\(.id):\(.name)"')
461
+
462
+ local total=$(echo "$campaigns" | wc -l)
463
+ local count=0
464
+
465
+ while IFS=: read -r campaign_id campaign_name; do
466
+ ((count++))
467
+
468
+ # Sanitize filename
469
+ local safe_name=$(echo "$campaign_name" | \
470
+ tr '[:upper:]' '[:lower:]' | \
471
+ tr -cs '[:alnum:]' '-' | \
472
+ sed 's/-*$//')
473
+
474
+ local filename="${output_dir}/campaign-${campaign_id}-${safe_name}"
475
+
476
+ echo "[$count/$total] Generating report for: $campaign_name"
477
+
478
+ # Get campaign report
479
+ cakemail reports campaign "$campaign_id" -f json > "${filename}.json"
480
+
481
+ # Get link analytics
482
+ cakemail reports campaign-links "$campaign_id" -f json > "${filename}-links.json"
483
+
484
+ # Create summary CSV
485
+ jq -r '["metric","value"],
486
+ (["Delivered",.delivered],
487
+ ["Open Rate",(.unique_opens / .delivered * 100 | round)],
488
+ ["Click Rate",(.unique_clicks / .delivered * 100 | round)],
489
+ ["Bounce Rate",(.bounced / .total_recipients * 100 | round)]) |
490
+ @csv' "${filename}.json" > "${filename}-summary.csv"
491
+
492
+ echo "✓ Saved report: $filename"
493
+
494
+ sleep 1
495
+ done <<< "$campaigns"
496
+
497
+ # Create combined report
498
+ echo "Creating combined report..."
499
+
500
+ echo "campaign_id,campaign_name,delivered,unique_opens,unique_clicks,open_rate,click_rate" > \
501
+ "${output_dir}/combined-report.csv"
502
+
503
+ for json_file in "$output_dir"/campaign-*.json; do
504
+ [ -f "$json_file" ] || continue
505
+ [[ "$json_file" == *"-links.json" ]] && continue
506
+
507
+ jq -r --arg id "$(basename "$json_file" | cut -d- -f2)" \
508
+ --arg name "$(basename "$json_file" .json | cut -d- -f3-)" \
509
+ '[$id, $name, .delivered, .unique_opens, .unique_clicks,
510
+ (.unique_opens / .delivered * 100 | round),
511
+ (.unique_clicks / .delivered * 100 | round)] | @csv' \
512
+ "$json_file" >> "${output_dir}/combined-report.csv"
513
+ done
514
+
515
+ echo "Reports complete: $output_dir"
516
+ }
517
+
518
+ bulk_report_generation 30
519
+ ```
520
+
521
+ ### Performance Comparison Report
522
+
523
+ ```bash
524
+ #!/bin/bash
525
+
526
+ # Compare performance across multiple campaigns
527
+ compare_campaigns() {
528
+ local -a campaign_ids=("$@")
529
+ local output_file="campaign-comparison-$(date +%Y%m%d).csv"
530
+
531
+ # CSV header
532
+ echo "id,name,delivered,open_rate,click_rate,ctor,bounce_rate,unsubscribe_rate" > "$output_file"
533
+
534
+ for campaign_id in "${campaign_ids[@]}"; do
535
+ echo "Fetching data for campaign $campaign_id..."
536
+
537
+ # Get campaign data
538
+ local data=$(cakemail reports campaign "$campaign_id" -f json)
539
+
540
+ # Extract metrics
541
+ local name=$(echo "$data" | jq -r '.campaign_name' | tr ',' ';')
542
+ local delivered=$(echo "$data" | jq -r '.delivered')
543
+ local recipients=$(echo "$data" | jq -r '.total_recipients')
544
+ local unique_opens=$(echo "$data" | jq -r '.unique_opens')
545
+ local unique_clicks=$(echo "$data" | jq -r '.unique_clicks')
546
+ local bounced=$(echo "$data" | jq -r '.bounced')
547
+ local unsubscribed=$(echo "$data" | jq -r '.unsubscribed')
548
+
549
+ # Calculate rates
550
+ local open_rate=$(echo "scale=2; $unique_opens * 100 / $delivered" | bc)
551
+ local click_rate=$(echo "scale=2; $unique_clicks * 100 / $delivered" | bc)
552
+ local ctor=$(echo "scale=2; $unique_clicks * 100 / $unique_opens" | bc)
553
+ local bounce_rate=$(echo "scale=2; $bounced * 100 / $recipients" | bc)
554
+ local unsub_rate=$(echo "scale=2; $unsubscribed * 100 / $delivered" | bc)
555
+
556
+ # Write to CSV
557
+ echo "$campaign_id,$name,$delivered,$open_rate,$click_rate,$ctor,$bounce_rate,$unsub_rate" >> \
558
+ "$output_file"
559
+
560
+ sleep 1
561
+ done
562
+
563
+ echo "Comparison report saved to: $output_file"
564
+
565
+ # Display summary
566
+ echo ""
567
+ echo "Performance Summary:"
568
+ column -t -s',' "$output_file"
569
+ }
570
+
571
+ # Compare specific campaigns
572
+ compare_campaigns 790 791 792 793 794
573
+ ```
574
+
575
+ ## Data Synchronization
576
+
577
+ ### Two-Way Sync with External CRM
578
+
579
+ ```bash
580
+ #!/bin/bash
581
+
582
+ # Sync contacts between Cakemail and external CRM
583
+ sync_with_crm() {
584
+ local list_id="$1"
585
+ local crm_api="https://api.yourcrm.com"
586
+ local sync_state_file=".sync-state"
587
+
588
+ echo "Starting two-way sync..."
589
+
590
+ # 1. Export from Cakemail
591
+ echo "Exporting from Cakemail..."
592
+ local cakemail_contacts=$(mktemp)
593
+ cakemail contacts list "$list_id" -f json | \
594
+ jq -r '.data[] | "\(.email)|\(.updated_at)"' > "$cakemail_contacts"
595
+
596
+ # 2. Fetch from CRM
597
+ echo "Fetching from CRM..."
598
+ local crm_contacts=$(mktemp)
599
+ curl -s "$crm_api/contacts" | \
600
+ jq -r '.[] | "\(.email)|\(.updated_at)"' > "$crm_contacts"
601
+
602
+ # 3. Find contacts to update in CRM (modified in Cakemail)
603
+ echo "Finding Cakemail → CRM updates..."
604
+ while IFS='|' read -r email cakemail_updated; do
605
+ local crm_updated=$(grep "^${email}|" "$crm_contacts" | cut -d'|' -f2)
606
+
607
+ if [ -z "$crm_updated" ]; then
608
+ echo "New contact in Cakemail: $email"
609
+ # Add to CRM
610
+ cakemail contacts get-by-email "$list_id" "$email" -f json | \
611
+ curl -s -X POST "$crm_api/contacts" -H "Content-Type: application/json" -d @-
612
+ elif [[ "$cakemail_updated" > "$crm_updated" ]]; then
613
+ echo "Updating CRM: $email"
614
+ # Update CRM
615
+ cakemail contacts get-by-email "$list_id" "$email" -f json | \
616
+ curl -s -X PUT "$crm_api/contacts/$email" -H "Content-Type: application/json" -d @-
617
+ fi
618
+ done < "$cakemail_contacts"
619
+
620
+ # 4. Find contacts to update in Cakemail (modified in CRM)
621
+ echo "Finding CRM → Cakemail updates..."
622
+ while IFS='|' read -r email crm_updated; do
623
+ local cakemail_updated=$(grep "^${email}|" "$cakemail_contacts" | cut -d'|' -f2)
624
+
625
+ if [ -z "$cakemail_updated" ]; then
626
+ echo "New contact in CRM: $email"
627
+ # Add to Cakemail
628
+ local contact_data=$(curl -s "$crm_api/contacts/$email")
629
+ local first_name=$(echo "$contact_data" | jq -r '.first_name')
630
+ local last_name=$(echo "$contact_data" | jq -r '.last_name')
631
+
632
+ cakemail contacts add "$list_id" -e "$email" -f "$first_name" -l "$last_name"
633
+ elif [[ "$crm_updated" > "$cakemail_updated" ]]; then
634
+ echo "Updating Cakemail: $email"
635
+ # Update Cakemail
636
+ local contact_data=$(curl -s "$crm_api/contacts/$email")
637
+ local first_name=$(echo "$contact_data" | jq -r '.first_name')
638
+ local last_name=$(echo "$contact_data" | jq -r '.last_name')
639
+
640
+ cakemail contacts update "$list_id" "$email" -f "$first_name" -l "$last_name"
641
+ fi
642
+ done < "$crm_contacts"
643
+
644
+ # Cleanup
645
+ rm "$cakemail_contacts" "$crm_contacts"
646
+
647
+ # Save sync timestamp
648
+ date -Iseconds > "$sync_state_file"
649
+
650
+ echo "Sync complete"
651
+ }
652
+
653
+ # Usage
654
+ sync_with_crm 123
655
+ ```
656
+
657
+ ### Deduplicate Contacts Across Lists
658
+
659
+ ```bash
660
+ #!/bin/bash
661
+
662
+ # Find and remove duplicate contacts across multiple lists
663
+ deduplicate_contacts() {
664
+ local -a list_ids=("$@")
665
+ local temp_file=$(mktemp)
666
+
667
+ echo "Finding duplicates across ${#list_ids[@]} lists..."
668
+
669
+ # Export all contacts with list IDs
670
+ for list_id in "${list_ids[@]}"; do
671
+ echo "Scanning list $list_id..."
672
+ cakemail contacts list "$list_id" -f json | \
673
+ jq -r --arg list "$list_id" \
674
+ '.data[] | "\(.email)|\($list)|\(.id)"' >> "$temp_file"
675
+ done
676
+
677
+ # Find duplicates
678
+ echo "Analyzing duplicates..."
679
+ local duplicates=$(cut -d'|' -f1 "$temp_file" | sort | uniq -d)
680
+
681
+ if [ -z "$duplicates" ]; then
682
+ echo "No duplicates found"
683
+ rm "$temp_file"
684
+ return
685
+ fi
686
+
687
+ echo "Found duplicates:"
688
+ echo "$duplicates"
689
+ echo ""
690
+
691
+ # Process each duplicate
692
+ while read -r email; do
693
+ echo "Processing: $email"
694
+
695
+ # Get all occurrences
696
+ local occurrences=$(grep "^${email}|" "$temp_file")
697
+ local count=$(echo "$occurrences" | wc -l)
698
+
699
+ echo " Found in $count lists:"
700
+ echo "$occurrences" | while IFS='|' read -r dup_email list_id contact_id; do
701
+ echo " List $list_id (Contact $contact_id)"
702
+ done
703
+
704
+ # Keep first occurrence, remove others
705
+ local first=true
706
+ echo "$occurrences" | while IFS='|' read -r dup_email list_id contact_id; do
707
+ if [ "$first" = true ]; then
708
+ echo " ✓ Keeping in list $list_id"
709
+ first=false
710
+ else
711
+ echo " ✗ Removing from list $list_id"
712
+ cakemail contacts delete "$list_id" "$contact_id"
713
+ fi
714
+ done
715
+
716
+ echo ""
717
+ done <<< "$duplicates"
718
+
719
+ rm "$temp_file"
720
+
721
+ echo "Deduplication complete"
722
+ }
723
+
724
+ # Usage
725
+ deduplicate_contacts 123 124 125
726
+ ```
727
+
728
+ ## Memory-Efficient Processing
729
+
730
+ ### Stream Processing Large Exports
731
+
732
+ ```bash
733
+ #!/bin/bash
734
+
735
+ # Process large export without loading into memory
736
+ stream_process_export() {
737
+ local list_id="$1"
738
+ local export_id="$2"
739
+
740
+ echo "Streaming export $export_id..."
741
+
742
+ # Process line by line
743
+ cakemail contacts export-download "$export_id" | {
744
+ local line_num=0
745
+ local processed=0
746
+
747
+ while IFS=, read -r email first_name last_name status custom_attrs; do
748
+ ((line_num++))
749
+
750
+ # Skip header
751
+ [ $line_num -eq 1 ] && continue
752
+
753
+ # Process contact (example: extract bounced emails)
754
+ if [ "$status" = "bounced" ]; then
755
+ echo "$email" >> bounced-emails.txt
756
+ ((processed++))
757
+ fi
758
+
759
+ # Progress indicator
760
+ if [ $((line_num % 1000)) -eq 0 ]; then
761
+ echo "Processed $line_num contacts ($processed bounced)"
762
+ fi
763
+ done
764
+
765
+ echo "Stream processing complete: $processed bounced emails found"
766
+ }
767
+ }
768
+
769
+ # Usage
770
+ stream_process_export 123 "export_abc123"
771
+ ```
772
+
773
+ ### Paginated List Processing
774
+
775
+ ```bash
776
+ #!/bin/bash
777
+
778
+ # Process all contacts using pagination
779
+ paginated_processing() {
780
+ local list_id="$1"
781
+ local page_size=100
782
+ local page=1
783
+ local total_processed=0
784
+
785
+ while true; do
786
+ echo "Processing page $page (size: $page_size)..."
787
+
788
+ # Fetch page
789
+ local response=$(cakemail contacts list "$list_id" \
790
+ --page "$page" \
791
+ --per-page "$page_size" \
792
+ -f json)
793
+
794
+ # Check if empty
795
+ local count=$(echo "$response" | jq '.data | length')
796
+
797
+ if [ "$count" -eq 0 ]; then
798
+ echo "No more contacts"
799
+ break
800
+ fi
801
+
802
+ # Process contacts on this page
803
+ echo "$response" | jq -r '.data[] | .email' | while read -r email; do
804
+ # Your processing logic here
805
+ echo "Processing: $email"
806
+ ((total_processed++))
807
+ done
808
+
809
+ echo "Page $page complete ($count contacts)"
810
+
811
+ ((page++))
812
+ sleep 1
813
+ done
814
+
815
+ echo "Total processed: $total_processed contacts"
816
+ }
817
+
818
+ paginated_processing 123
819
+ ```
820
+
821
+ ## Error Recovery
822
+
823
+ ### Bulk Operation with Transaction Log
824
+
825
+ ```bash
826
+ #!/bin/bash
827
+
828
+ # Perform bulk operation with detailed logging for rollback
829
+ bulk_operation_with_log() {
830
+ local operation="$1"
831
+ shift
832
+ local items=("$@")
833
+
834
+ local log_file="bulk-operation-$(date +%Y%m%d-%H%M%S).log"
835
+ local success_count=0
836
+ local failure_count=0
837
+
838
+ echo "Starting bulk operation: $operation" | tee -a "$log_file"
839
+ echo "Total items: ${#items[@]}" | tee -a "$log_file"
840
+ echo "---" | tee -a "$log_file"
841
+
842
+ for item in "${items[@]}"; do
843
+ echo "[$(date -Iseconds)] Processing: $item" >> "$log_file"
844
+
845
+ if eval "$operation '$item'"; then
846
+ ((success_count++))
847
+ echo "SUCCESS|$item" >> "$log_file"
848
+ else
849
+ ((failure_count++))
850
+ echo "FAILURE|$item" >> "$log_file"
851
+ fi
852
+ done
853
+
854
+ echo "---" | tee -a "$log_file"
855
+ echo "Complete: $success_count success, $failure_count failures" | tee -a "$log_file"
856
+ echo "Log saved to: $log_file"
857
+
858
+ # Return failure count
859
+ return $failure_count
860
+ }
861
+
862
+ # Example: Delete multiple campaigns
863
+ delete_campaign() {
864
+ local campaign_id="$1"
865
+ cakemail campaigns delete "$campaign_id"
866
+ }
867
+
868
+ bulk_operation_with_log "delete_campaign" 790 791 792 793
869
+ ```
870
+
871
+ ### Retry Failed Operations
872
+
873
+ ```bash
874
+ #!/bin/bash
875
+
876
+ # Retry operations that failed in previous bulk run
877
+ retry_failed_operations() {
878
+ local log_file="$1"
879
+ local operation="$2"
880
+
881
+ if [ ! -f "$log_file" ]; then
882
+ echo "Log file not found: $log_file"
883
+ return 1
884
+ fi
885
+
886
+ # Extract failed items
887
+ local failed_items=$(grep "^FAILURE|" "$log_file" | cut -d'|' -f2)
888
+ local count=$(echo "$failed_items" | wc -w)
889
+
890
+ if [ $count -eq 0 ]; then
891
+ echo "No failed operations to retry"
892
+ return 0
893
+ fi
894
+
895
+ echo "Retrying $count failed operations..."
896
+
897
+ local retry_log="retry-$(basename "$log_file")"
898
+ local success=0
899
+
900
+ for item in $failed_items; do
901
+ echo "Retrying: $item"
902
+
903
+ if eval "$operation '$item'"; then
904
+ ((success++))
905
+ echo "SUCCESS|$item" >> "$retry_log"
906
+ else
907
+ echo "FAILURE|$item" >> "$retry_log"
908
+ fi
909
+
910
+ sleep 2 # Longer delay for retries
911
+ done
912
+
913
+ echo "Retry complete: $success/$count succeeded"
914
+ }
915
+
916
+ # Usage
917
+ retry_failed_operations "bulk-operation-20240315-143022.log" "delete_campaign"
918
+ ```
919
+
920
+ ## Performance Tips
921
+
922
+ 1. **Use Batch Operations**: Import 1000 contacts at once vs. 1000 individual calls
923
+ 2. **Implement Pagination**: Process large datasets in chunks
924
+ 3. **Add Rate Limiting**: Sleep between operations to avoid throttling
925
+ 4. **Use Parallel Processing**: Run independent operations concurrently (max 3-5 jobs)
926
+ 5. **Stream Large Files**: Process line-by-line instead of loading into memory
927
+ 6. **Cache API Responses**: Store frequently accessed data locally
928
+ 7. **Use Filters**: Reduce data transfer with server-side filtering
929
+ 8. **Implement Checkpoints**: Resume long operations after interruption
930
+ 9. **Log Everything**: Track progress and enable recovery
931
+ 10. **Monitor Resources**: Watch memory and disk usage for long operations
932
+