Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,26 @@ conf/rspamd/local.d/redis.conf
ssl-self-signed/cert.pem
ssl-self-signed/key.pem
core/billionmail*

# Server-specific runtime configs
conf/askai/
conf/chat/
conf/supplier/
conf/tokens_cache/
conf/postfix/conf/
conf/postfix/main.cf
conf/postfix/master.cf
conf/rspamd/local.d/worker-controller.inc
conf/dovecot/conf.d/10-ssl.conf
conf/dovecot/conf.d/20-pop3.conf
conf/dovecot/conf.d/90-quota.conf
conf/dovecot/dovecot.conf

# Build/runtime artifacts
core/frontend/package-lock.json
core/frontend/.pnpm-store/
core/frontend/node_modules/
core/frontend/dist/
core/public/dist/
docker-compose.yml.bak*
docker-compose_*.yml
120 changes: 108 additions & 12 deletions bm.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,60 @@ MODIFY_SAFE_ENTRANCE() {
fi
}

# Rebuild Frontend only
REBUILD_FRONTEND() {
echo "Rebuilding BillionMail Frontend..."
echo -e "\033[33mThis will rebuild the frontend UI and restart the core container.\033[0m"

# Check if frontend directory exists
if [ ! -d "./core/frontend" ]; then
echo -e "\033[31mError: Frontend directory not found at ./core/frontend\033[0m"
exit 1
fi

echo "Step 1/4: Building frontend with Node.js (this may take a few minutes)..."

# Use Docker to build the frontend (no need for Node.js installed on host)
docker run --rm \
-v "$(pwd)/core/frontend:/app" \
-w /app \
node:20-alpine \
sh -c "npm install -g pnpm && pnpm install && pnpm run build"

if [ $? -eq 0 ]; then
echo -e "\033[32m✓ Frontend build completed successfully\033[0m"
else
echo -e "\033[31m✗ Frontend build failed\033[0m"
exit 1
fi

echo "Step 2/4: Copying build files to public directory..."
if [ -d "./core/frontend/dist" ]; then
# Clean old build files first
rm -rf ./core/public/dist/*
# Copy new build
cp -r ./core/frontend/dist/* ./core/public/dist/
echo -e "\033[32m✓ Build files copied to core/public/dist/\033[0m"
else
echo -e "\033[31m✗ Build directory not found\033[0m"
exit 1
fi

echo "Step 3/4: Restarting core container to load new frontend..."
SERVICE="core"
GET_SERVICE_NAME
if [ -n "${SERVICE_NAME}" ]; then
${DOCKER_COMPOSE} restart ${SERVICE_NAME}
echo -e "\033[32m✓ Core container restarted\033[0m"
else
echo -e "\033[31m✗ Core service not found\033[0m"
exit 1
fi

echo "Step 4/4: Clearing browser cache recommended..."
echo -e "\033[32m✓ Frontend rebuild completed! Please refresh your browser (Ctrl+Shift+R or Cmd+Shift+R)\033[0m"
}

# Restart the BillionMail project
RESTART_PROJECT() {
echo "Restarting BillionMail..."
Expand Down Expand Up @@ -1270,7 +1324,12 @@ APPLY_MULTI_IP() {
else
echo "📋 Found $(wc -l < "$TEMP_FILE") configuration records"

# Temporarily disable ERR trap for the loop to prevent early exit
trap - ERR

# Safely process query results
INSERTED_COUNT=0
FAILED_COUNT=0
while IFS='|' read -r domain smtp_name; do
# Clean whitespace and validate
domain=$(echo "$domain" | xargs)
Expand All @@ -1292,33 +1351,43 @@ APPLY_MULTI_IP() {

# Use secure SQL query
escaped_domain=$(printf '%s\n' "$domain_with_at" | sed "s/'/''/g")
EXISTS=$(docker exec -i -e PGPASSWORD=${DBPASS} ${PGSQL_CONTAINER_NAME} psql -U ${DBUSER} -d ${DBNAME} -t -A \
-c "SELECT 1 FROM bm_domain_smtp_transport WHERE domain = '$escaped_domain';")
EXISTS=$(docker exec -e PGPASSWORD=${DBPASS} ${PGSQL_CONTAINER_NAME} psql -U ${DBUSER} -d ${DBNAME} -t -A \
-c "SELECT 1 FROM bm_domain_smtp_transport WHERE domain = '$escaped_domain';" 2>/dev/null </dev/null || echo "")

# If exists, delete it
if [[ -n "$EXISTS" && "$EXISTS" != "" ]]; then
echo "🟡 Sender rule already exists for domain: $domain_with_at, deleting..."
if ! docker exec -i -e PGPASSWORD=${DBPASS} ${PGSQL_CONTAINER_NAME} psql -U ${DBUSER} -d ${DBNAME} \
-c "DELETE FROM bm_domain_smtp_transport WHERE domain = '$escaped_domain';"; then
Red_Error "❌ Failed to delete old record: $domain_with_at"
if docker exec -e PGPASSWORD=${DBPASS} ${PGSQL_CONTAINER_NAME} psql -U ${DBUSER} -d ${DBNAME} \
-c "DELETE FROM bm_domain_smtp_transport WHERE domain = '$escaped_domain';" 2>/dev/null </dev/null; then
echo "✅ Old rule deleted: $domain_with_at"
else
echo "⚠️ Warning: Could not delete old record for $domain_with_at, continuing..."
fi
echo "✅ Old rule deleted: $domain_with_at"
fi

# Execute insertion (using secure string escaping)
echo "📝 Inserting: $domain_with_at → $smtp_name"
escaped_smtp=$(printf '%s\n' "$smtp_name" | sed "s/'/''/g")
escaped_atype=$(printf '%s\n' "$atype" | sed "s/'/''/g")
if ! docker exec -i -e PGPASSWORD=${DBPASS} ${PGSQL_CONTAINER_NAME} psql -U ${DBUSER} -d ${DBNAME} \
-c "INSERT INTO bm_domain_smtp_transport (atype, domain, smtp_name) VALUES ('$escaped_atype', '$escaped_domain', '$escaped_smtp');"; then
Red_Error "❌ Insertion failed: $domain_with_at"
if docker exec -e PGPASSWORD=${DBPASS} ${PGSQL_CONTAINER_NAME} psql -U ${DBUSER} -d ${DBNAME} \
-c "INSERT INTO bm_domain_smtp_transport (atype, domain, smtp_name) VALUES ('$escaped_atype', '$escaped_domain', '$escaped_smtp');" 2>/dev/null </dev/null; then
echo "✅ Successfully inserted: $domain_with_at → $smtp_name"
((INSERTED_COUNT++))
else
echo "❌ Failed to insert: $domain_with_at"
((FAILED_COUNT++))
fi

echo "✅ Successfully inserted: $domain_with_at → $smtp_name"

done < "$TEMP_FILE"

echo "📝 All domain mappings have been successfully written to bm_domain_smtp_transport"
# Re-enable ERR trap
trap 'echo "� Script execution failed, triggering rollback"; rollback_compose; cleanup_apply; exit 1' ERR

echo "📝 Domain mapping summary: $INSERTED_COUNT inserted, $FAILED_COUNT failed"

if [[ $FAILED_COUNT -gt 0 ]]; then
echo "⚠️ Warning: Some domains failed to insert, but continuing..."
fi
fi

# Update status of all records in bm_multi_ip_domain table to 'applied'
Expand All @@ -1343,6 +1412,26 @@ APPLY_MULTI_IP() {
echo "✅ All multi-IP domain statuses are already 'applied'"
fi

# ============ 3.5. Add dedicated IPs to interface ============
if [[ -s "$TEMP_FILE" ]]; then
INTERFACE=$(ip route | grep default | awk '{print $5}')
echo "🔧 Adding dedicated IPs to interface $INTERFACE..."
while IFS='|' read -r domain smtp_name; do
domain=$(echo "$domain" | xargs)
smtp_name=$(echo "$smtp_name" | xargs)
[[ -z "$domain" || -z "$smtp_name" ]] && continue
IP=$(echo "$smtp_name" | sed 's/smtp_bind_ip_\([0-9.]*\)_.*$/\1/')
if [[ "$IP" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
if ! ip addr show dev $INTERFACE | grep -q $IP; then
ip addr add $IP/32 dev $INTERFACE
echo "✅ Added IP $IP to $INTERFACE"
else
echo "ℹ️ IP $IP already on $INTERFACE"
fi
fi
done < "$TEMP_FILE"
fi

# ============ 4. Restart services ============
echo "🔄 Restarting BillionMail services"
if ! ${DOCKER_COMPOSE} down; then
Expand Down Expand Up @@ -1389,6 +1478,9 @@ APPLY_MULTI_IP() {

echo "========================================="

echo "⚠️💡❌ Remember: Run 'sudo vi /etc/netplan/50-cloud-init.yaml', add the IPs under addresses, and 'sudo netplan apply' for persistence."
echo "⚠️💡❌ Remember to Check ?: Check ip addr show dev ens3 | grep \"inet \", you need to see your new ip"

# Successfully completed, remove ERR trap
trap - ERR
}
Expand Down Expand Up @@ -1599,6 +1691,7 @@ SHOW_HELP() {
echo " status - Show BillionMail containers running status : $0 status"
echo " down - Stop and remove containers, networks: $0 down"
echo " rebuild - Rebuild all BillionMail containers: $0 rebuild"
echo " rebuild-frontend - Rebuild frontend UI only: $0 rebuild-frontend"
echo " top - Show all BillionMail processes: $0 top"
echo " ps - Show all BillionMail containers: $0 ps"
echo " service-top - Show processes of a specific BillionMail service: $0 s-t postfix"
Expand Down Expand Up @@ -1676,6 +1769,9 @@ case "$1" in
REBUILD_PROJECT|rebuild_project|rebuild)
REBUILD_PROJECT
;;
REBUILD_FRONTEND|rebuild_frontend|rebuild-frontend)
REBUILD_FRONTEND
;;
RESTART_PROJECT|restart_project|restart)
RESTART_PROJECT
;;
Expand Down
1 change: 1 addition & 0 deletions core/api/batch_mail/v1/batch_mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ type CreateTaskReq struct {
Threads int `json:"threads" v:"min:0" dc:"threads" default:"5"`
TrackOpen int `json:"track_open" v:"in:0,1" dc:"track open" default:"1"`
TrackClick int `json:"track_click" v:"in:0,1" dc:"track click" default:"1"`
RotateSenders int `json:"rotate_senders" v:"in:0,1" dc:"rotate through all domain mailboxes" default:"0"`
StartTime int `json:"start_time" v:"required" dc:"start time"`
Warmup int `json:"warmup" v:"in:0,1" dc:"warmup" default:"0"`
Remark string `json:"remark" dc:"remark"`
Expand Down
1 change: 1 addition & 0 deletions core/frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
# Dist
node_modules
dist/
.pnpm-store/

# IDE
.vscode/*
Expand Down
1 change: 1 addition & 0 deletions core/frontend/src/i18n/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@
"ipWarmupTip": "Enabling this will limit the sending rate",
"trackClick": "Track Clicks",
"trackOpen": "Track Opens",
"rotateSenders": "Rotate Senders",
"unsubscribeLink": "Unsubscribe link",
"threads": "Threads",
"threadsAuto": "Auto",
Expand Down
1 change: 1 addition & 0 deletions core/frontend/src/i18n/lang/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@
"ipWarmupTip": "有効にすると送信速度が制限されます",
"trackClick": "クリック追跡",
"trackOpen": "開封追跡",
"rotateSenders": "送信者ローテーション",
"preview": {
"empty": "プレビューするメールテンプレートを選択してください"
},
Expand Down
1 change: 1 addition & 0 deletions core/frontend/src/i18n/lang/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@
"ipWarmupTip": "开启后将会限制发件速率",
"trackClick": "跟踪点击",
"trackOpen": "跟踪打开",
"rotateSenders": "轮换发件人",
"unsubscribeLink": "退订链接",
"threads": "线程数",
"threadsAuto": "自动",
Expand Down
7 changes: 7 additions & 0 deletions core/frontend/src/views/market/task/edit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@
<n-switch v-model:value="form.track_open" :checked-value="1" :unchecked-value="0">
</n-switch>
</n-form-item-gi>
<n-form-item-gi :span="12" :label="t('market.task.edit.rotateSenders')" path="rotate_senders">
<n-switch v-model:value="form.rotate_senders" :checked-value="1" :unchecked-value="0">
</n-switch>
</n-form-item-gi>
</n-grid>

<n-form-item :label="$t('market.task.edit.threads')" :show-feedback="false">
Expand Down Expand Up @@ -212,6 +216,7 @@ const form = reactive({
tag_logic: 'OR',
track_click: 1,
track_open: 1,
rotate_senders: 0,
})

const logicOptions = [
Expand Down Expand Up @@ -376,6 +381,7 @@ const getParams = () => {
return {
track_open: form.track_open,
track_click: form.track_click,
rotate_senders: form.rotate_senders,
addresser: form.addresser || '',
full_name: form.full_name,
subject: form.subject,
Expand Down Expand Up @@ -446,6 +452,7 @@ const initForm = async () => {
form.tag_logic = res.tag_logic
form.track_open = res.track_open
form.track_click = res.track_click
form.rotate_senders = res.rotate_senders || 0
nextTick(() => {
form.tag_ids = res.tag_ids
})
Expand Down
1 change: 1 addition & 0 deletions core/internal/model/entity/batch_mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ type EmailTask struct {
TagIds []int `json:"tag_ids" dc:"Tag IDs (parsed array)"`
TagLogic string `json:"tag_logic" dc:"Tag Logic (AND/OR/NOT)"`
UseTagFilter int `json:"use_tag_filter" dc:"Use Tag Filter (0: no, 1: yes)"`
RotateSenders int `json:"rotate_senders" dc:"Rotate through all domain mailboxes (0: no, 1: yes)"`
}

// MarshalJSON implements custom JSON marshaling to convert TagIdsRaw to TagIds array
Expand Down
8 changes: 6 additions & 2 deletions core/internal/service/batch_mail/api_mail_send.go
Original file line number Diff line number Diff line change
Expand Up @@ -434,8 +434,12 @@ func sendApiMail(ctx context.Context, apiTemplate *entity.ApiTemplates, subject
baseURL := domains.GetBaseURL()
apiTemplate_id := apiTemplate.Id + 1000000000
mailTracker := maillog_stat.NewMailTracker(content, apiTemplate_id, messageId, log.Recipient, baseURL)
mailTracker.TrackLinks()
mailTracker.AppendTrackingPixel()
if apiTemplate.TrackClick == 1 {
mailTracker.TrackLinks()
}
if apiTemplate.TrackOpen == 1 {
mailTracker.AppendTrackingPixel()
}
content = mailTracker.GetHTML()

// create email message
Expand Down
1 change: 1 addition & 0 deletions core/internal/service/batch_mail/batch_mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ func CreateTaskWithRecipients(ctx context.Context, req *v1.CreateTaskReq, addTyp
"threads": req.Threads,
"track_open": req.TrackOpen,
"track_click": req.TrackClick,
"rotate_senders": req.RotateSenders,
"start_time": req.StartTime,
"create_time": now,
"update_time": now,
Expand Down
Loading