@cakemail-org/cakemail-cli 1.7.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 +41 -37
- package/audit-formats.js +128 -0
- package/cakemail.rb +20 -0
- package/dist/client.js +1 -1
- 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.js +1 -1
- package/dist/commands/campaigns.js.map +1 -1
- package/dist/commands/contacts.js +1 -1
- 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.js +1 -1
- package/dist/commands/interests.js.map +1 -1
- package/dist/commands/lists.js +1 -1
- package/dist/commands/lists.js.map +1 -1
- package/dist/commands/logs.js +1 -1
- package/dist/commands/logs.js.map +1 -1
- 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.js +1 -1
- 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.js +1 -1
- package/dist/commands/tags.js.map +1 -1
- package/dist/commands/templates.js +1 -1
- package/dist/commands/templates.js.map +1 -1
- package/dist/commands/transactional-templates.js +1 -1
- package/dist/commands/transactional-templates.js.map +1 -1
- package/dist/commands/webhooks.js +1 -1
- package/dist/commands/webhooks.js.map +1 -1
- package/dist/utils/config.js +2 -2
- 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/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 -61
- 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,672 @@
|
|
|
1
|
+
# Scripting Patterns & Best Practices
|
|
2
|
+
|
|
3
|
+
Advanced patterns and techniques for building robust automation scripts with the Cakemail CLI.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Learn to:
|
|
8
|
+
- Build reliable automation scripts
|
|
9
|
+
- Handle errors gracefully
|
|
10
|
+
- Implement retry logic
|
|
11
|
+
- Process data efficiently
|
|
12
|
+
- Create reusable functions
|
|
13
|
+
- Follow best practices
|
|
14
|
+
|
|
15
|
+
## Basic Script Structure
|
|
16
|
+
|
|
17
|
+
### Complete Script Template
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
#!/bin/bash
|
|
21
|
+
|
|
22
|
+
# Script: campaign-automation.sh
|
|
23
|
+
# Description: Automated campaign workflow
|
|
24
|
+
# Usage: ./campaign-automation.sh <list-id>
|
|
25
|
+
|
|
26
|
+
set -euo pipefail # Exit on error, undefined vars, pipe failures
|
|
27
|
+
|
|
28
|
+
# Configuration
|
|
29
|
+
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
30
|
+
readonly LOG_FILE="${SCRIPT_DIR}/automation.log"
|
|
31
|
+
readonly ERROR_EMAIL="admin@company.com"
|
|
32
|
+
|
|
33
|
+
# Functions
|
|
34
|
+
log() {
|
|
35
|
+
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
error_exit() {
|
|
39
|
+
log "ERROR: $1"
|
|
40
|
+
echo "Script failed: $1" | mail -s "Automation Error" "$ERROR_EMAIL"
|
|
41
|
+
exit 1
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Main script
|
|
45
|
+
main() {
|
|
46
|
+
local list_id="$1"
|
|
47
|
+
|
|
48
|
+
log "Starting automation for list $list_id"
|
|
49
|
+
|
|
50
|
+
# Your logic here
|
|
51
|
+
if ! cakemail lists get "$list_id" &>/dev/null; then
|
|
52
|
+
error_exit "List $list_id not found"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
log "Automation completed successfully"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Validate arguments
|
|
59
|
+
if [ $# -ne 1 ]; then
|
|
60
|
+
echo "Usage: $0 <list-id>"
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
main "$@"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Error Handling Patterns
|
|
68
|
+
|
|
69
|
+
### Retry Logic
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
#!/bin/bash
|
|
73
|
+
|
|
74
|
+
# Retry function with exponential backoff
|
|
75
|
+
retry() {
|
|
76
|
+
local max_attempts=3
|
|
77
|
+
local attempt=1
|
|
78
|
+
local delay=2
|
|
79
|
+
|
|
80
|
+
while [ $attempt -le $max_attempts ]; do
|
|
81
|
+
if "$@"; then
|
|
82
|
+
return 0
|
|
83
|
+
else
|
|
84
|
+
if [ $attempt -lt $max_attempts ]; then
|
|
85
|
+
log "Attempt $attempt failed. Retrying in ${delay}s..."
|
|
86
|
+
sleep $delay
|
|
87
|
+
delay=$((delay * 2)) # Exponential backoff
|
|
88
|
+
((attempt++))
|
|
89
|
+
else
|
|
90
|
+
log "All $max_attempts attempts failed"
|
|
91
|
+
return 1
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
done
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Usage
|
|
98
|
+
if retry cakemail campaigns create -n "Test" -l 123 -s 101; then
|
|
99
|
+
log "Campaign created successfully"
|
|
100
|
+
else
|
|
101
|
+
error_exit "Failed to create campaign after retries"
|
|
102
|
+
fi
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Graceful Degradation
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
#!/bin/bash
|
|
109
|
+
|
|
110
|
+
# Try operation, fallback if it fails
|
|
111
|
+
create_campaign_with_fallback() {
|
|
112
|
+
local list_id="$1"
|
|
113
|
+
local template_id="$2"
|
|
114
|
+
local fallback_template="$3"
|
|
115
|
+
|
|
116
|
+
# Try with preferred template
|
|
117
|
+
if campaign_id=$(cakemail campaigns create \
|
|
118
|
+
-n "Newsletter" \
|
|
119
|
+
-l "$list_id" \
|
|
120
|
+
-s 101 \
|
|
121
|
+
--template "$template_id" \
|
|
122
|
+
-f json 2>/dev/null | jq -r '.id'); then
|
|
123
|
+
echo "$campaign_id"
|
|
124
|
+
return 0
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# Fallback to basic template
|
|
128
|
+
log "Primary template failed, using fallback"
|
|
129
|
+
campaign_id=$(cakemail campaigns create \
|
|
130
|
+
-n "Newsletter" \
|
|
131
|
+
-l "$list_id" \
|
|
132
|
+
-s 101 \
|
|
133
|
+
--template "$fallback_template" \
|
|
134
|
+
-f json | jq -r '.id')
|
|
135
|
+
echo "$campaign_id"
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Cleanup on Exit
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
#!/bin/bash
|
|
143
|
+
|
|
144
|
+
# Temporary files
|
|
145
|
+
temp_files=()
|
|
146
|
+
|
|
147
|
+
cleanup() {
|
|
148
|
+
log "Cleaning up temporary files..."
|
|
149
|
+
for file in "${temp_files[@]}"; do
|
|
150
|
+
[ -f "$file" ] && rm -f "$file"
|
|
151
|
+
done
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Register cleanup
|
|
155
|
+
trap cleanup EXIT
|
|
156
|
+
|
|
157
|
+
# Create temp file
|
|
158
|
+
temp_file=$(mktemp)
|
|
159
|
+
temp_files+=("$temp_file")
|
|
160
|
+
|
|
161
|
+
# Use temp file
|
|
162
|
+
cakemail contacts export 123 > "$temp_file"
|
|
163
|
+
|
|
164
|
+
# Cleanup happens automatically on exit
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Data Processing Patterns
|
|
168
|
+
|
|
169
|
+
### Batch Processing
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
#!/bin/bash
|
|
173
|
+
|
|
174
|
+
# Process items in batches
|
|
175
|
+
batch_process() {
|
|
176
|
+
local items=("$@")
|
|
177
|
+
local batch_size=10
|
|
178
|
+
local total=${#items[@]}
|
|
179
|
+
local processed=0
|
|
180
|
+
|
|
181
|
+
for ((i=0; i<total; i+=batch_size)); do
|
|
182
|
+
local batch=("${items[@]:i:batch_size}")
|
|
183
|
+
|
|
184
|
+
log "Processing batch $((i/batch_size + 1)) (${#batch[@]} items)"
|
|
185
|
+
|
|
186
|
+
for item in "${batch[@]}"; do
|
|
187
|
+
process_item "$item"
|
|
188
|
+
((processed++))
|
|
189
|
+
done
|
|
190
|
+
|
|
191
|
+
# Progress update
|
|
192
|
+
local percent=$((processed * 100 / total))
|
|
193
|
+
log "Progress: $processed/$total ($percent%)"
|
|
194
|
+
|
|
195
|
+
# Rate limiting
|
|
196
|
+
sleep 1
|
|
197
|
+
done
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
process_item() {
|
|
201
|
+
local email="$1"
|
|
202
|
+
cakemail contacts add 123 -e "$email"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# Read emails from file
|
|
206
|
+
mapfile -t emails < emails.txt
|
|
207
|
+
batch_process "${emails[@]}"
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Parallel Processing
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
#!/bin/bash
|
|
214
|
+
|
|
215
|
+
# Process items in parallel (with limit)
|
|
216
|
+
parallel_process() {
|
|
217
|
+
local max_jobs=5
|
|
218
|
+
local items=("$@")
|
|
219
|
+
|
|
220
|
+
for item in "${items[@]}"; do
|
|
221
|
+
# Wait if too many jobs running
|
|
222
|
+
while [ $(jobs -r | wc -l) -ge $max_jobs ]; do
|
|
223
|
+
sleep 0.1
|
|
224
|
+
done
|
|
225
|
+
|
|
226
|
+
# Process in background
|
|
227
|
+
process_item "$item" &
|
|
228
|
+
done
|
|
229
|
+
|
|
230
|
+
# Wait for all jobs to complete
|
|
231
|
+
wait
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
process_item() {
|
|
235
|
+
local campaign_id="$1"
|
|
236
|
+
cakemail reports campaign "$campaign_id" -f json > "report-${campaign_id}.json"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
# Process all campaigns in parallel
|
|
240
|
+
campaigns=(790 791 792 793 794)
|
|
241
|
+
parallel_process "${campaigns[@]}"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Streaming Large Datasets
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
#!/bin/bash
|
|
248
|
+
|
|
249
|
+
# Stream process large CSV without loading into memory
|
|
250
|
+
stream_process_csv() {
|
|
251
|
+
local input_file="$1"
|
|
252
|
+
local line_count=0
|
|
253
|
+
|
|
254
|
+
# Skip header
|
|
255
|
+
tail -n +2 "$input_file" | while IFS=, read -r email first_name last_name; do
|
|
256
|
+
# Process each line
|
|
257
|
+
cakemail contacts add 123 -e "$email" -f "$first_name" -l "$last_name"
|
|
258
|
+
|
|
259
|
+
((line_count++))
|
|
260
|
+
if [ $((line_count % 100)) -eq 0 ]; then
|
|
261
|
+
log "Processed $line_count contacts"
|
|
262
|
+
fi
|
|
263
|
+
done
|
|
264
|
+
|
|
265
|
+
log "Total processed: $line_count contacts"
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
stream_process_csv large-contact-list.csv
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Reusable Functions
|
|
272
|
+
|
|
273
|
+
### Configuration Loading
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
#!/bin/bash
|
|
277
|
+
|
|
278
|
+
# Load configuration from file
|
|
279
|
+
load_config() {
|
|
280
|
+
local config_file="${1:-.cakemail.conf}"
|
|
281
|
+
|
|
282
|
+
if [ ! -f "$config_file" ]; then
|
|
283
|
+
error_exit "Config file not found: $config_file"
|
|
284
|
+
fi
|
|
285
|
+
|
|
286
|
+
# Source config file
|
|
287
|
+
# shellcheck source=/dev/null
|
|
288
|
+
source "$config_file"
|
|
289
|
+
|
|
290
|
+
# Validate required variables
|
|
291
|
+
: "${LIST_ID:?LIST_ID not set in config}"
|
|
292
|
+
: "${SENDER_ID:?SENDER_ID not set in config}"
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
# Config file: .cakemail.conf
|
|
296
|
+
# LIST_ID=123
|
|
297
|
+
# SENDER_ID=101
|
|
298
|
+
# TEMPLATE_ID=201
|
|
299
|
+
|
|
300
|
+
load_config
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### JSON Parsing
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
#!/bin/bash
|
|
307
|
+
|
|
308
|
+
# Extract values from JSON responses
|
|
309
|
+
get_json_value() {
|
|
310
|
+
local json="$1"
|
|
311
|
+
local key="$2"
|
|
312
|
+
echo "$json" | jq -r ".$key"
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
# Get multiple values
|
|
316
|
+
parse_campaign_response() {
|
|
317
|
+
local response="$1"
|
|
318
|
+
|
|
319
|
+
campaign_id=$(get_json_value "$response" "id")
|
|
320
|
+
campaign_name=$(get_json_value "$response" "name")
|
|
321
|
+
campaign_status=$(get_json_value "$response" "status")
|
|
322
|
+
|
|
323
|
+
echo "ID: $campaign_id, Name: $campaign_name, Status: $campaign_status"
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# Usage
|
|
327
|
+
response=$(cakemail campaigns get 790 -f json)
|
|
328
|
+
parse_campaign_response "$response"
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Date Handling
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
#!/bin/bash
|
|
335
|
+
|
|
336
|
+
# Calculate dates for filtering
|
|
337
|
+
get_date_range() {
|
|
338
|
+
local days_ago="$1"
|
|
339
|
+
|
|
340
|
+
# Start date (N days ago)
|
|
341
|
+
start_date=$(date -d "$days_ago days ago" +%Y-%m-%d)
|
|
342
|
+
|
|
343
|
+
# End date (today)
|
|
344
|
+
end_date=$(date +%Y-%m-%d)
|
|
345
|
+
|
|
346
|
+
echo "$start_date $end_date"
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
# Get campaigns from last 30 days
|
|
350
|
+
read -r start end <<< "$(get_date_range 30)"
|
|
351
|
+
cakemail campaigns list --filter "delivered_at>=$start;delivered_at<=$end"
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Progress Indicators
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
#!/bin/bash
|
|
358
|
+
|
|
359
|
+
# Show progress bar
|
|
360
|
+
show_progress() {
|
|
361
|
+
local current="$1"
|
|
362
|
+
local total="$2"
|
|
363
|
+
local width=50
|
|
364
|
+
|
|
365
|
+
local percent=$((current * 100 / total))
|
|
366
|
+
local filled=$((width * current / total))
|
|
367
|
+
local empty=$((width - filled))
|
|
368
|
+
|
|
369
|
+
printf "\rProgress: [%${filled}s%${empty}s] %d%%" | tr ' ' '='
|
|
370
|
+
printf "%${empty}s %d/%d" "" "$current" "$total"
|
|
371
|
+
|
|
372
|
+
if [ "$current" -eq "$total" ]; then
|
|
373
|
+
echo ""
|
|
374
|
+
fi
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
# Usage
|
|
378
|
+
total=100
|
|
379
|
+
for i in $(seq 1 $total); do
|
|
380
|
+
# Process item
|
|
381
|
+
sleep 0.1
|
|
382
|
+
show_progress $i $total
|
|
383
|
+
done
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Validation Patterns
|
|
387
|
+
|
|
388
|
+
### Input Validation
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
#!/bin/bash
|
|
392
|
+
|
|
393
|
+
# Validate email format
|
|
394
|
+
validate_email() {
|
|
395
|
+
local email="$1"
|
|
396
|
+
local regex='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
|
397
|
+
|
|
398
|
+
if [[ ! $email =~ $regex ]]; then
|
|
399
|
+
return 1
|
|
400
|
+
fi
|
|
401
|
+
return 0
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
# Validate required parameters
|
|
405
|
+
validate_params() {
|
|
406
|
+
local list_id="$1"
|
|
407
|
+
local sender_id="$2"
|
|
408
|
+
|
|
409
|
+
if [ -z "$list_id" ]; then
|
|
410
|
+
error_exit "List ID is required"
|
|
411
|
+
fi
|
|
412
|
+
|
|
413
|
+
if ! [[ $list_id =~ ^[0-9]+$ ]]; then
|
|
414
|
+
error_exit "List ID must be numeric"
|
|
415
|
+
fi
|
|
416
|
+
|
|
417
|
+
# Verify list exists
|
|
418
|
+
if ! cakemail lists get "$list_id" &>/dev/null; then
|
|
419
|
+
error_exit "List $list_id not found"
|
|
420
|
+
fi
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Pre-flight Checks
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
#!/bin/bash
|
|
428
|
+
|
|
429
|
+
# Run pre-flight checks before main operations
|
|
430
|
+
preflight_checks() {
|
|
431
|
+
log "Running pre-flight checks..."
|
|
432
|
+
|
|
433
|
+
# Check CLI is installed
|
|
434
|
+
if ! command -v cakemail &>/dev/null; then
|
|
435
|
+
error_exit "Cakemail CLI not found. Install with: npm install -g @cakemail-org/cakemail-cli"
|
|
436
|
+
fi
|
|
437
|
+
|
|
438
|
+
# Check authentication
|
|
439
|
+
if ! cakemail account test &>/dev/null; then
|
|
440
|
+
error_exit "Authentication failed. Run: cakemail config init"
|
|
441
|
+
fi
|
|
442
|
+
|
|
443
|
+
# Check dependencies
|
|
444
|
+
for cmd in jq curl; do
|
|
445
|
+
if ! command -v $cmd &>/dev/null; then
|
|
446
|
+
error_exit "Required command not found: $cmd"
|
|
447
|
+
fi
|
|
448
|
+
done
|
|
449
|
+
|
|
450
|
+
# Check disk space
|
|
451
|
+
local available=$(df -BG . | tail -1 | awk '{print $4}' | tr -d 'G')
|
|
452
|
+
if [ "$available" -lt 1 ]; then
|
|
453
|
+
error_exit "Insufficient disk space (need at least 1GB)"
|
|
454
|
+
fi
|
|
455
|
+
|
|
456
|
+
log "All pre-flight checks passed"
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Logging Patterns
|
|
461
|
+
|
|
462
|
+
### Structured Logging
|
|
463
|
+
|
|
464
|
+
```bash
|
|
465
|
+
#!/bin/bash
|
|
466
|
+
|
|
467
|
+
# Log levels
|
|
468
|
+
readonly LOG_LEVEL_DEBUG=0
|
|
469
|
+
readonly LOG_LEVEL_INFO=1
|
|
470
|
+
readonly LOG_LEVEL_WARN=2
|
|
471
|
+
readonly LOG_LEVEL_ERROR=3
|
|
472
|
+
|
|
473
|
+
# Current log level
|
|
474
|
+
LOG_LEVEL=${LOG_LEVEL:-$LOG_LEVEL_INFO}
|
|
475
|
+
|
|
476
|
+
log_debug() { [ $LOG_LEVEL -le $LOG_LEVEL_DEBUG ] && log "DEBUG: $*"; }
|
|
477
|
+
log_info() { [ $LOG_LEVEL -le $LOG_LEVEL_INFO ] && log "INFO: $*"; }
|
|
478
|
+
log_warn() { [ $LOG_LEVEL -le $LOG_LEVEL_WARN ] && log "WARN: $*"; }
|
|
479
|
+
log_error() { [ $LOG_LEVEL -le $LOG_LEVEL_ERROR ] && log "ERROR: $*"; }
|
|
480
|
+
|
|
481
|
+
# Usage
|
|
482
|
+
LOG_LEVEL=$LOG_LEVEL_DEBUG # Show all logs
|
|
483
|
+
log_debug "Debug message"
|
|
484
|
+
log_info "Info message"
|
|
485
|
+
log_warn "Warning message"
|
|
486
|
+
log_error "Error message"
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Log Rotation
|
|
490
|
+
|
|
491
|
+
```bash
|
|
492
|
+
#!/bin/bash
|
|
493
|
+
|
|
494
|
+
# Rotate log file if too large
|
|
495
|
+
rotate_log() {
|
|
496
|
+
local log_file="$1"
|
|
497
|
+
local max_size=$((10 * 1024 * 1024)) # 10MB
|
|
498
|
+
|
|
499
|
+
if [ -f "$log_file" ]; then
|
|
500
|
+
local size=$(stat -f%z "$log_file" 2>/dev/null || stat -c%s "$log_file" 2>/dev/null)
|
|
501
|
+
|
|
502
|
+
if [ "$size" -gt "$max_size" ]; then
|
|
503
|
+
local timestamp=$(date +%Y%m%d-%H%M%S)
|
|
504
|
+
mv "$log_file" "${log_file}.${timestamp}"
|
|
505
|
+
log "Log rotated: ${log_file}.${timestamp}"
|
|
506
|
+
fi
|
|
507
|
+
fi
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
# Rotate before starting
|
|
511
|
+
rotate_log "$LOG_FILE"
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
## Testing Patterns
|
|
515
|
+
|
|
516
|
+
### Dry Run Mode
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
#!/bin/bash
|
|
520
|
+
|
|
521
|
+
# Support dry-run mode
|
|
522
|
+
DRY_RUN=${DRY_RUN:-false}
|
|
523
|
+
|
|
524
|
+
execute() {
|
|
525
|
+
local command="$*"
|
|
526
|
+
|
|
527
|
+
if [ "$DRY_RUN" = true ]; then
|
|
528
|
+
log "[DRY RUN] Would execute: $command"
|
|
529
|
+
return 0
|
|
530
|
+
else
|
|
531
|
+
log "Executing: $command"
|
|
532
|
+
eval "$command"
|
|
533
|
+
fi
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
# Usage
|
|
537
|
+
execute cakemail campaigns create -n "Test" -l 123 -s 101
|
|
538
|
+
|
|
539
|
+
# Run in dry-run mode
|
|
540
|
+
DRY_RUN=true ./script.sh
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### Mock Responses
|
|
544
|
+
|
|
545
|
+
```bash
|
|
546
|
+
#!/bin/bash
|
|
547
|
+
|
|
548
|
+
# Mock mode for testing
|
|
549
|
+
MOCK_MODE=${MOCK_MODE:-false}
|
|
550
|
+
|
|
551
|
+
cakemail_wrapper() {
|
|
552
|
+
if [ "$MOCK_MODE" = true ]; then
|
|
553
|
+
# Return mock data
|
|
554
|
+
case "$1" in
|
|
555
|
+
campaigns)
|
|
556
|
+
echo '{"id": 999, "name": "Mock Campaign", "status": "draft"}'
|
|
557
|
+
;;
|
|
558
|
+
lists)
|
|
559
|
+
echo '{"id": 123, "name": "Mock List", "contacts_count": 100}'
|
|
560
|
+
;;
|
|
561
|
+
*)
|
|
562
|
+
echo '{"success": true}'
|
|
563
|
+
;;
|
|
564
|
+
esac
|
|
565
|
+
else
|
|
566
|
+
# Execute real command
|
|
567
|
+
cakemail "$@"
|
|
568
|
+
fi
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
# Use wrapper instead of direct calls
|
|
572
|
+
response=$(cakemail_wrapper campaigns get 790 -f json)
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
## Performance Patterns
|
|
576
|
+
|
|
577
|
+
### Caching Results
|
|
578
|
+
|
|
579
|
+
```bash
|
|
580
|
+
#!/bin/bash
|
|
581
|
+
|
|
582
|
+
# Cache results to avoid redundant API calls
|
|
583
|
+
CACHE_DIR=".cache"
|
|
584
|
+
mkdir -p "$CACHE_DIR"
|
|
585
|
+
|
|
586
|
+
cached_call() {
|
|
587
|
+
local cache_key="$1"
|
|
588
|
+
shift
|
|
589
|
+
local command="$*"
|
|
590
|
+
local cache_file="$CACHE_DIR/$cache_key"
|
|
591
|
+
local cache_ttl=3600 # 1 hour
|
|
592
|
+
|
|
593
|
+
# Check if cached and not expired
|
|
594
|
+
if [ -f "$cache_file" ]; then
|
|
595
|
+
local age=$(($(date +%s) - $(stat -f%m "$cache_file" 2>/dev/null || stat -c%Y "$cache_file")))
|
|
596
|
+
if [ "$age" -lt "$cache_ttl" ]; then
|
|
597
|
+
cat "$cache_file"
|
|
598
|
+
return 0
|
|
599
|
+
fi
|
|
600
|
+
fi
|
|
601
|
+
|
|
602
|
+
# Execute and cache
|
|
603
|
+
eval "$command" | tee "$cache_file"
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
# Usage - list will be cached for 1 hour
|
|
607
|
+
lists=$(cached_call "lists" cakemail lists list -f json)
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
### Rate Limiting
|
|
611
|
+
|
|
612
|
+
```bash
|
|
613
|
+
#!/bin/bash
|
|
614
|
+
|
|
615
|
+
# Rate limiter using token bucket algorithm
|
|
616
|
+
declare -A rate_limit_tokens
|
|
617
|
+
declare -A rate_limit_last_refill
|
|
618
|
+
|
|
619
|
+
rate_limit() {
|
|
620
|
+
local key="${1:-default}"
|
|
621
|
+
local max_tokens=10
|
|
622
|
+
local refill_rate=1 # tokens per second
|
|
623
|
+
local current_time=$(date +%s)
|
|
624
|
+
|
|
625
|
+
# Initialize if needed
|
|
626
|
+
if [ -z "${rate_limit_tokens[$key]}" ]; then
|
|
627
|
+
rate_limit_tokens[$key]=$max_tokens
|
|
628
|
+
rate_limit_last_refill[$key]=$current_time
|
|
629
|
+
fi
|
|
630
|
+
|
|
631
|
+
# Refill tokens
|
|
632
|
+
local elapsed=$((current_time - rate_limit_last_refill[$key]))
|
|
633
|
+
local new_tokens=$((elapsed * refill_rate))
|
|
634
|
+
if [ $new_tokens -gt 0 ]; then
|
|
635
|
+
rate_limit_tokens[$key]=$((rate_limit_tokens[$key] + new_tokens))
|
|
636
|
+
if [ ${rate_limit_tokens[$key]} -gt $max_tokens ]; then
|
|
637
|
+
rate_limit_tokens[$key]=$max_tokens
|
|
638
|
+
fi
|
|
639
|
+
rate_limit_last_refill[$key]=$current_time
|
|
640
|
+
fi
|
|
641
|
+
|
|
642
|
+
# Check if token available
|
|
643
|
+
if [ ${rate_limit_tokens[$key]} -gt 0 ]; then
|
|
644
|
+
rate_limit_tokens[$key]=$((rate_limit_tokens[$key] - 1))
|
|
645
|
+
return 0
|
|
646
|
+
else
|
|
647
|
+
# Wait for token
|
|
648
|
+
sleep 1
|
|
649
|
+
rate_limit "$key"
|
|
650
|
+
fi
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
# Usage
|
|
654
|
+
for i in {1..20}; do
|
|
655
|
+
rate_limit "api_calls"
|
|
656
|
+
cakemail campaigns list
|
|
657
|
+
done
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
## Best Practices
|
|
661
|
+
|
|
662
|
+
1. **Use strict mode**: `set -euo pipefail`
|
|
663
|
+
2. **Validate all inputs**: Check before processing
|
|
664
|
+
3. **Handle errors gracefully**: Don't fail silently
|
|
665
|
+
4. **Log everything**: Comprehensive logging
|
|
666
|
+
5. **Use functions**: Modular, reusable code
|
|
667
|
+
6. **Quote variables**: Prevent word splitting
|
|
668
|
+
7. **Check exit codes**: Test command success
|
|
669
|
+
8. **Clean up resources**: Temp files, background jobs
|
|
670
|
+
9. **Document scripts**: Clear comments
|
|
671
|
+
10. **Test thoroughly**: Dry-run and mock modes
|
|
672
|
+
|