@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.
- package/.claude/settings.local.json +12 -0
- package/.env.example +40 -0
- package/.env.test.example +45 -0
- package/CHANGELOG.md +1031 -0
- package/README.md +319 -15
- package/audit-formats.js +128 -0
- package/cakemail.rb +20 -0
- package/dist/cli.js +27 -10
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +2 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +16 -6
- package/dist/client.js.map +1 -1
- package/dist/commands/account.js +1 -1
- package/dist/commands/account.js.map +1 -1
- package/dist/commands/attributes.js +1 -1
- package/dist/commands/attributes.js.map +1 -1
- package/dist/commands/campaigns.d.ts.map +1 -1
- package/dist/commands/campaigns.js +103 -8
- package/dist/commands/campaigns.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +63 -4
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/contacts.d.ts.map +1 -1
- package/dist/commands/contacts.js +91 -12
- package/dist/commands/contacts.js.map +1 -1
- package/dist/commands/emails.js +1 -1
- package/dist/commands/emails.js.map +1 -1
- package/dist/commands/interests.d.ts +5 -0
- package/dist/commands/interests.d.ts.map +1 -0
- package/dist/commands/interests.js +172 -0
- package/dist/commands/interests.js.map +1 -0
- package/dist/commands/lists.d.ts.map +1 -1
- package/dist/commands/lists.js +6 -8
- package/dist/commands/lists.js.map +1 -1
- package/dist/commands/logs.d.ts +5 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +237 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/reports.js +1 -1
- package/dist/commands/reports.js.map +1 -1
- package/dist/commands/segments.js +1 -1
- package/dist/commands/segments.js.map +1 -1
- package/dist/commands/senders.d.ts.map +1 -1
- package/dist/commands/senders.js +11 -8
- package/dist/commands/senders.js.map +1 -1
- package/dist/commands/suppressed.js +1 -1
- package/dist/commands/suppressed.js.map +1 -1
- package/dist/commands/tags.d.ts +5 -0
- package/dist/commands/tags.d.ts.map +1 -0
- package/dist/commands/tags.js +124 -0
- package/dist/commands/tags.js.map +1 -0
- package/dist/commands/templates.js +1 -1
- package/dist/commands/templates.js.map +1 -1
- package/dist/commands/transactional-templates.d.ts +5 -0
- package/dist/commands/transactional-templates.d.ts.map +1 -0
- package/dist/commands/transactional-templates.js +354 -0
- package/dist/commands/transactional-templates.js.map +1 -0
- package/dist/commands/webhooks.js +1 -1
- package/dist/commands/webhooks.js.map +1 -1
- package/dist/utils/auth.d.ts +8 -1
- package/dist/utils/auth.d.ts.map +1 -1
- package/dist/utils/auth.js +39 -11
- package/dist/utils/auth.js.map +1 -1
- package/dist/utils/config-file.d.ts +7 -0
- package/dist/utils/config-file.d.ts.map +1 -1
- package/dist/utils/config-file.js +15 -0
- package/dist/utils/config-file.js.map +1 -1
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +12 -4
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/errors.js +1 -1
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/list-defaults.d.ts +33 -0
- package/dist/utils/list-defaults.d.ts.map +1 -0
- package/dist/utils/list-defaults.js +52 -0
- package/dist/utils/list-defaults.js.map +1 -0
- package/dist/utils/output.d.ts.map +1 -1
- package/dist/utils/output.js +36 -13
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/progress.d.ts.map +1 -1
- package/dist/utils/progress.js +32 -4
- package/dist/utils/progress.js.map +1 -1
- package/dist/utils/spinner.d.ts +17 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js +43 -0
- package/dist/utils/spinner.js.map +1 -0
- package/docs/DOCUMENTATION-STANDARD.md +1068 -0
- package/docs/README.md +161 -0
- package/docs/developer/ARCHITECTURE.md +516 -0
- package/docs/developer/AUTH.md +204 -0
- package/docs/developer/CONTRIBUTING.md +227 -0
- package/docs/developer/DOCUMENTATION_SUMMARY.md +346 -0
- package/docs/developer/PROJECT_INDEX.md +365 -0
- package/docs/planning/API_COVERAGE.md +1045 -0
- package/docs/planning/BACKLOG.md +1159 -0
- package/docs/planning/PROFILE_SYSTEM_TASKS.md +287 -0
- package/docs/planning/UX_IMPLEMENTATION_PLAN.md +691 -0
- package/docs/planning/archive/RELEASE_CHECKLIST_v1.3.0.md +332 -0
- package/docs/planning/archive/RELEASE_v1.3.0.md +428 -0
- package/docs/planning/archive/cakemail-cli-ux-improvements.md +438 -0
- package/docs/planning/cakemail-profile-system-plan.md +1121 -0
- package/docs/testing/AI_USER_SIMULATION_DESIGN.md +1342 -0
- package/docs/testing/KENOGAMI_BIDIRECTIONAL_FLOW.md +1517 -0
- package/docs/testing/KENOGAMI_TRUTH_RECONCILIATION_SYSTEM.md +1369 -0
- package/docs/user-manual/.obsidian/app.json +1 -0
- package/docs/user-manual/.obsidian/appearance.json +1 -0
- package/docs/user-manual/.obsidian/core-plugins.json +33 -0
- package/docs/user-manual/.obsidian/workspace.json +167 -0
- package/docs/user-manual/01-getting-started/01-installation.md +214 -0
- package/docs/user-manual/01-getting-started/02-quick-start.md +432 -0
- package/docs/user-manual/01-getting-started/03-authentication.md +448 -0
- package/docs/user-manual/01-getting-started/04-configuration.md +430 -0
- package/docs/user-manual/01-getting-started/05-output-formats.md +447 -0
- package/docs/user-manual/02-core-concepts/01-accounts.md +514 -0
- package/docs/user-manual/02-core-concepts/02-profile-system.md +771 -0
- package/docs/user-manual/02-core-concepts/03-smart-defaults.md +485 -0
- package/docs/user-manual/02-core-concepts/04-authentication-methods.md +435 -0
- package/docs/user-manual/02-core-concepts/05-pagination-filtering.md +600 -0
- package/docs/user-manual/02-core-concepts/06-error-handling.md +718 -0
- package/docs/user-manual/02-core-concepts/07-api-coverage.md +483 -0
- package/docs/user-manual/03-email-operations/01-senders.md +490 -0
- package/docs/user-manual/03-email-operations/02-templates.md +444 -0
- package/docs/user-manual/03-email-operations/03-transactional-emails.md +706 -0
- package/docs/user-manual/03-email-operations/04-email-tracking.md +407 -0
- package/docs/user-manual/04-campaign-management/01-campaigns-basics.md +394 -0
- package/docs/user-manual/04-campaign-management/02-campaign-scheduling.md +630 -0
- package/docs/user-manual/04-campaign-management/03-campaign-testing.md +997 -0
- package/docs/user-manual/04-campaign-management/04-campaign-lifecycle.md +709 -0
- package/docs/user-manual/04-campaign-management/05-campaign-links.md +934 -0
- package/docs/user-manual/05-contact-management/01-lists.md +836 -0
- package/docs/user-manual/05-contact-management/02-contacts.md +1035 -0
- package/docs/user-manual/05-contact-management/03-custom-attributes.md +788 -0
- package/docs/user-manual/05-contact-management/04-segments.md +1028 -0
- package/docs/user-manual/05-contact-management/05-contact-import-export.md +1031 -0
- package/docs/user-manual/06-analytics-reporting/01-campaign-analytics.md +867 -0
- package/docs/user-manual/06-analytics-reporting/02-account-reports.md +227 -0
- package/docs/user-manual/07-integrations/01-webhooks-integration.md +259 -0
- package/docs/user-manual/07-integrations/02-automation.md +326 -0
- package/docs/user-manual/08-advanced-usage/01-scripting-patterns.md +672 -0
- package/docs/user-manual/08-advanced-usage/02-bulk-operations.md +932 -0
- package/docs/user-manual/08-advanced-usage/03-ci-cd-integration.md +892 -0
- package/docs/user-manual/08-advanced-usage/04-performance-optimization.md +766 -0
- package/docs/user-manual/09-command-reference/01-config.md +776 -0
- package/docs/user-manual/09-command-reference/02-account.md +652 -0
- package/docs/user-manual/09-command-reference/03-lists.md +958 -0
- package/docs/user-manual/09-command-reference/04-contacts.md +1408 -0
- package/docs/user-manual/09-command-reference/05-attributes.md +617 -0
- package/docs/user-manual/09-command-reference/06-segments.md +894 -0
- package/docs/user-manual/09-command-reference/07-senders.md +803 -0
- package/docs/user-manual/09-command-reference/08-templates.md +818 -0
- package/docs/user-manual/09-command-reference/09-campaigns.md +1250 -0
- package/docs/user-manual/09-command-reference/10-emails.md +807 -0
- package/docs/user-manual/09-command-reference/11-reports.md +1135 -0
- package/docs/user-manual/09-command-reference/12-webhooks.md +773 -0
- package/docs/user-manual/09-command-reference/13-suppressed.md +797 -0
- package/docs/user-manual/09-command-reference/14-interests.md +630 -0
- package/docs/user-manual/09-command-reference/15-tags.md +584 -0
- package/docs/user-manual/09-command-reference/16-logs.md +656 -0
- package/docs/user-manual/09-command-reference/17-transactional-templates.md +850 -0
- package/docs/user-manual/10-troubleshooting/01-common-errors.md +457 -0
- package/docs/user-manual/10-troubleshooting/02-authentication-issues.md +558 -0
- package/docs/user-manual/10-troubleshooting/03-connection-problems.md +634 -0
- package/docs/user-manual/10-troubleshooting/04-debugging.md +725 -0
- package/docs/user-manual/11-appendix/04-faq.md +484 -0
- package/docs/user-manual/11-appendix/05-glossary.md +250 -0
- package/docs/user-manual/README.md +0 -0
- package/package.json +13 -47
- package/src/cli.ts +125 -0
- package/src/client.ts +16 -0
- package/src/commands/account.ts +267 -0
- package/src/commands/accounts.ts +78 -0
- package/src/commands/actions.ts +249 -0
- package/src/commands/attributes.ts +139 -0
- package/src/commands/campaign-blueprints.ts +106 -0
- package/src/commands/campaigns.ts +469 -0
- package/src/commands/config.ts +77 -0
- package/src/commands/contacts.ts +612 -0
- package/src/commands/custom-attributes.ts +127 -0
- package/src/commands/dkims.ts +117 -0
- package/src/commands/domains.ts +82 -0
- package/src/commands/email-apis.ts +569 -0
- package/src/commands/emails.ts +197 -0
- package/src/commands/forms.ts +283 -0
- package/src/commands/interests.ts +155 -0
- package/src/commands/links.ts +38 -0
- package/src/commands/lists.ts +406 -0
- package/src/commands/logos.ts +71 -0
- package/src/commands/logs.ts +386 -0
- package/src/commands/reports.ts +306 -0
- package/src/commands/segments.ts +158 -0
- package/src/commands/senders.ts +204 -0
- package/src/commands/sub-accounts.ts +271 -0
- package/src/commands/suppressed-emails.ts +234 -0
- package/src/commands/suppressed.ts +198 -0
- package/src/commands/system-emails.ts +85 -0
- package/src/commands/tags.ts +146 -0
- package/src/commands/tasks.ts +116 -0
- package/src/commands/templates.ts +189 -0
- package/src/commands/tokens.ts +83 -0
- package/src/commands/transactional-emails.ts +374 -0
- package/src/commands/transactional-templates.ts +385 -0
- package/src/commands/users.ts +506 -0
- package/src/commands/webhooks.ts +172 -0
- package/src/commands/workflow-blueprints.ts +123 -0
- package/src/commands/workflows.ts +265 -0
- package/src/types/profile.ts +93 -0
- package/src/utils/auth.ts +272 -0
- package/src/utils/config-file.ts +96 -0
- package/src/utils/config.ts +134 -0
- package/src/utils/confirm.ts +32 -0
- package/src/utils/defaults.ts +99 -0
- package/src/utils/errors.ts +116 -0
- package/src/utils/interactive.ts +91 -0
- package/src/utils/list-defaults.ts +74 -0
- package/src/utils/output.ts +190 -0
- package/src/utils/progress.ts +320 -0
- package/src/utils/spinner.ts +22 -0
- package/tests/IMPLEMENTATION_STATUS.md +258 -0
- package/tests/PTY_SETUP.md +118 -0
- package/tests/PTY_TESTING_GUIDE.md +507 -0
- package/tests/README.md +244 -0
- package/tests/fixtures/api-responses/campaigns.json +34 -0
- package/tests/fixtures/test-config.json +13 -0
- package/tests/helpers/cli-runner.ts +128 -0
- package/tests/helpers/mock-server.ts +301 -0
- package/tests/helpers/pty-runner.ts +181 -0
- package/tests/integration/campaigns-real-api.test.ts +196 -0
- package/tests/integration/setup-integration.ts +50 -0
- package/tests/pty/campaigns.test.ts +241 -0
- package/tests/setup.ts +34 -0
- package/tsconfig.json +15 -0
- 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
|
+
|