configuration/findUsersWithSessionInMonths.sh
... ...
@@ -0,0 +1,27 @@
1
+#!/bin/bash
2
+# Usage: ${0} {MONGO-URI} {MONTHS}
3
+# Example: findUsersWithSessionInMonths.sh "mongodb://localhost/winddb?replicaSet=rs0" 12
4
+echo '
5
+var cutoff = new Date();
6
+cutoff.setMonth(cutoff.getMonth() - '${2}');
7
+
8
+var activeUsers = db.SESSIONS.distinct(
9
+ "SESSION_ATTRIBUTES.SESSION_ATTRIBUTE_VALUE.SESSION_PRINCIPAL_REALM_VALUE",
10
+ { SESSION_START_TIMESTAMP: { $gte: cutoff } }
11
+).flat();
12
+
13
+db.USERS.find(
14
+ {
15
+ EMAIL_VALIDATED: true,
16
+ EMAIL: { $exists: true, $ne: "" },
17
+ NAME: { $in: activeUsers },
18
+ $or: [
19
+ { DID_OPT_OUT_OF_FEATURE_AND_COMMUNITY_EMAILS: { $exists: false } },
20
+ { DID_OPT_OUT_OF_FEATURE_AND_COMMUNITY_EMAILS: false }
21
+ ]
22
+ },
23
+ { EMAIL: 1, FULLNAME: 1, NAME: 1, _id: 0 }
24
+).forEach(function(u) {
25
+ var name = (u.FULLNAME && u.FULLNAME.trim()) || u.NAME || "user";
26
+ print(u.EMAIL + "," + name);
27
+})' | mongosh "${1}"
configuration/sendEmails.md
... ...
@@ -0,0 +1,89 @@
1
+# sendEmails.sh — Personalized Email Sender
2
+
3
+Bash script for sending personalized HTML emails via SMTP (AWS WorkMail).
4
+
5
+## Prerequisites
6
+
7
+- `curl` with SMTP/TLS support
8
+- `base64` (coreutils)
9
+- SMTP credentials for the WorkMail account (`support@sapsailing.com`)
10
+
11
+## Files
12
+
13
+| File | Purpose |
14
+|---|---|
15
+| `sendEmails.sh` | The sending script |
16
+| `emailTemplate.html` | HTML email template with `{{NAME}}` placeholder |
17
+| `recipients.csv` | CSV file with recipient list |
18
+
19
+## CSV Format
20
+
21
+```csv
22
+email,name
23
+alice@example.com,Alice
24
+bob@example.com,Bob
25
+sailor42@example.com,sailor42
26
+```
27
+
28
+- **email** — recipient address
29
+- **name** — inserted into the salutation ("Dear {{NAME}},"). Use the user's first name if known, otherwise their username/nickname.
30
+- The header row is auto-detected and skipped if the first field starts with `email` (case-insensitive).
31
+
32
+## Usage
33
+
34
+```bash
35
+./sendEmails.sh [--dry-run] <recipients.csv> [template.html]
36
+```
37
+
38
+| Argument | Required | Default | Description |
39
+|---|---|---|---|
40
+| `--dry-run` | no | — | Preview personalized emails without sending |
41
+| `recipients.csv` | yes | — | Path to the CSV file |
42
+| `template.html` | no | `/tmp/emailTemplate.html` | Path to the HTML template |
43
+
44
+## Environment Variables
45
+
46
+| Variable | Required | Description |
47
+|---|---|---|
48
+| `SMTP_USER` | yes (unless `--dry-run`) | SMTP username for WorkMail |
49
+| `SMTP_PASS` | yes (unless `--dry-run`) | SMTP password for WorkMail |
50
+
51
+## Examples
52
+
53
+Preview all emails without sending:
54
+
55
+```bash
56
+./sendEmails.sh --dry-run recipients.csv
57
+```
58
+
59
+Send using a custom template:
60
+
61
+```bash
62
+export SMTP_USER="your-smtp-username"
63
+export SMTP_PASS="your-smtp-password"
64
+./sendEmails.sh recipients.csv myTemplate.html
65
+```
66
+
67
+## Configuration
68
+
69
+These values are set at the top of the script and can be adjusted:
70
+
71
+| Variable | Default | Description |
72
+|---|---|---|
73
+| `FROM_ADDR` | `SAP Sailing Analytics <support@sapsailing.com>` | Sender display name and address |
74
+| `SUBJECT` | `Your SAP Sailing Analytics Account — Open Source Transition Update` | Email subject line |
75
+| `SMTP_URL` | `smtps://smtp.mail.eu-west-1.awsapps.com:465` | WorkMail SMTP endpoint |
76
+
77
+## How It Works
78
+
79
+1. Reads the CSV line by line, skipping the header row.
80
+2. For each recipient, replaces `{{NAME}}` in the HTML template with the name from the CSV.
81
+3. Constructs a MIME message with UTF-8 base64-encoded subject and body.
82
+4. Sends via `curl` over SMTPS to the WorkMail endpoint.
83
+5. Waits 1 second between sends to respect SES rate limits.
84
+
85
+## Recommended Workflow
86
+
87
+1. **Preview** — Run with `--dry-run` and review the output.
88
+2. **Test** — Send to your own address first (`echo "email,name" > test.csv && echo "you@example.com,YourName" >> test.csv`).
89
+3. **Send** — Run against the full recipient list.
configuration/sendEmails.sh
... ...
@@ -0,0 +1,135 @@
1
+#!/usr/bin/env bash
2
+set -euo pipefail
3
+
4
+FROM_ADDR="SAP Sailing Analytics <support@sapsailing.com>"
5
+SUBJECT="SAP Sailing Analytics — Open Source Transition Update"
6
+SMTP_URL="smtps://smtp.mail.eu-west-1.awsapps.com:465"
7
+
8
+usage() {
9
+ cat <<'EOF'
10
+Usage: sendEmails.sh [--dry-run] <recipients.csv> [template.html]
11
+
12
+ recipients.csv CSV file with columns: email,name
13
+ template.html HTML template with {{NAME}} placeholder
14
+ (default: /tmp/emailTemplate.html)
15
+ --dry-run Print personalized emails to stdout without sending
16
+
17
+Environment variables:
18
+ SMTP_USER SMTP username (required unless --dry-run)
19
+ SMTP_PASS SMTP password (required unless --dry-run)
20
+
21
+CSV format example:
22
+ email,name
23
+ alice@example.com,Alice
24
+ bob@example.com,Bob
25
+EOF
26
+ exit 1
27
+}
28
+
29
+DRY_RUN=false
30
+if [[ "${1:-}" == "--dry-run" ]]; then
31
+ DRY_RUN=true
32
+ shift
33
+fi
34
+
35
+CSV="${1:-}"
36
+TEMPLATE="${2:-/tmp/emailTemplate.html}"
37
+
38
+[[ -z "$CSV" ]] && usage
39
+[[ ! -f "$CSV" ]] && { echo "Error: CSV file not found: $CSV"; exit 1; }
40
+[[ ! -f "$TEMPLATE" ]] && { echo "Error: Template file not found: $TEMPLATE"; exit 1; }
41
+
42
+if [[ "$DRY_RUN" == false ]]; then
43
+ [[ -z "${SMTP_USER:-}" ]] && { echo "Error: SMTP_USER not set"; exit 1; }
44
+ [[ -z "${SMTP_PASS:-}" ]] && { echo "Error: SMTP_PASS not set"; exit 1; }
45
+fi
46
+
47
+TEMPLATE_BODY=$(cat "$TEMPLATE")
48
+
49
+SENT=0
50
+FAILED=0
51
+SKIPPED_HEADER=false
52
+
53
+while IFS= read -r line || [[ -n "$line" ]]; do
54
+ # Skip empty lines
55
+ [[ -z "${line// }" ]] && continue
56
+
57
+ # Skip header row
58
+ if [[ "$SKIPPED_HEADER" == false ]]; then
59
+ SKIPPED_HEADER=true
60
+ if echo "$line" | grep -qi "^email"; then
61
+ continue
62
+ fi
63
+ fi
64
+
65
+ EMAIL=$(echo "$line" | cut -d',' -f1 | xargs)
66
+ NAME=$(echo "$line" | cut -d',' -f2- | xargs)
67
+
68
+ [[ -z "$EMAIL" ]] && continue
69
+
70
+ BODY="${TEMPLATE_BODY//\{\{NAME\}\}/$NAME}"
71
+
72
+ if [[ "$DRY_RUN" == true ]]; then
73
+ echo "========================================"
74
+ echo "To: $EMAIL"
75
+ echo "From: $FROM_ADDR"
76
+ echo "Subject: $SUBJECT"
77
+ echo "----------------------------------------"
78
+ echo "$BODY"
79
+ echo ""
80
+ SENT=$((SENT + 1))
81
+ continue
82
+ fi
83
+
84
+ MIME_MSG=$(cat <<MIME
85
+From: $FROM_ADDR
86
+To: $EMAIL
87
+Subject: =?UTF-8?B?$(echo -n "$SUBJECT" | base64 -w0)?=
88
+MIME-Version: 1.0
89
+Content-Type: text/html; charset=UTF-8
90
+Content-Transfer-Encoding: base64
91
+
92
+$(echo -n "$BODY" | base64 -w76)
93
+MIME
94
+ )
95
+
96
+ RETRIES=0
97
+ MAX_RETRIES=3
98
+ SEND_OK=false
99
+ while [[ $RETRIES -le $MAX_RETRIES ]]; do
100
+ if echo "$MIME_MSG" | curl --silent --show-error \
101
+ --url "$SMTP_URL" \
102
+ --ssl-reqd \
103
+ --mail-from "support@sapsailing.com" \
104
+ --mail-rcpt "$EMAIL" \
105
+ --user "${SMTP_USER}:${SMTP_PASS}" \
106
+ --upload-file - 2>/tmp/curl_err.txt; then
107
+ echo "Sent to $EMAIL ($NAME)"
108
+ SENT=$((SENT + 1))
109
+ SEND_OK=true
110
+ break
111
+ else
112
+ if grep -q "421" /tmp/curl_err.txt 2>/dev/null; then
113
+ RETRIES=$((RETRIES + 1))
114
+ WAIT=$((RETRIES * 5))
115
+ echo " Rate limited, retrying in ${WAIT}s... (attempt $((RETRIES))/$MAX_RETRIES)" >&2
116
+ sleep "$WAIT"
117
+ else
118
+ break
119
+ fi
120
+ fi
121
+ done
122
+ if [[ "$SEND_OK" == false ]]; then
123
+ echo "FAILED: $EMAIL ($NAME)" >&2
124
+ FAILED=$((FAILED + 1))
125
+ fi
126
+
127
+ sleep 3
128
+done < "$CSV"
129
+
130
+echo ""
131
+if [[ "$DRY_RUN" == true ]]; then
132
+ echo "Dry run complete. $SENT emails previewed."
133
+else
134
+ echo "Done. Sent: $SENT, Failed: $FAILED"
135
+fi
java/com.sap.sailing.aiagent/src/com/sap/sailing/aiagent/impl/AIAgentImpl.java
... ...
@@ -15,6 +15,7 @@ import java.util.logging.Logger;
15 15
16 16
import org.apache.http.client.ClientProtocolException;
17 17
import org.apache.shiro.SecurityUtils;
18
+import org.apache.shiro.UnavailableSecurityManagerException;
18 19
import org.apache.shiro.authz.AuthorizationException;
19 20
import org.json.simple.parser.ParseException;
20 21
import org.osgi.util.tracker.ServiceTracker;
... ...
@@ -312,7 +313,13 @@ public class AIAgentImpl implements AIAgent {
312 313
for (final Leaderboard leaderboard : event.getLeaderboards()) {
313 314
addNewRaceColumnListenerToLeaderboard(leaderboard);
314 315
}
315
- logger.info("User "+SecurityUtils.getSubject().getPrincipal()+" activated AI comments for event "+event.getName()+" with ID "+event.getId());
316
+ Object principal;
317
+ try {
318
+ principal = SecurityUtils.getSubject().getPrincipal();
319
+ } catch (UnavailableSecurityManagerException e) {
320
+ principal = "null";
321
+ }
322
+ logger.info("User "+principal+" activated AI comments for event "+event.getName()+" with ID "+event.getId());
316 323
listeners.forEach(l->l.startedCommentingOnEvent(event));
317 324
}
318 325
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ResultImportUrlsListComposite.java
... ...
@@ -23,6 +23,7 @@ import com.sap.sse.common.Util.Pair;
23 23
import com.sap.sse.gwt.client.ErrorReporter;
24 24
import com.sap.sse.gwt.client.Notification;
25 25
import com.sap.sse.gwt.client.Notification.NotificationType;
26
+import com.sap.sse.gwt.client.async.MarkedAsyncCallback;
26 27
import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel;
27 28
import com.sap.sse.gwt.client.dialog.DataEntryDialog;
28 29
import com.sap.sse.security.ui.client.component.AccessControlledButtonPanel;
... ...
@@ -112,7 +113,8 @@ public class ResultImportUrlsListComposite extends Composite {
112 113
new DataEntryDialog.DialogCallback<UrlDTO>() {
113 114
@Override
114 115
public void ok(UrlDTO url) {
115
- sailingServiceWrite.addResultImportUrl(getSelectedProviderName(), url, new AsyncCallback<Void>() {
116
+ sailingServiceWrite.addResultImportUrl(getSelectedProviderName(), url,
117
+ new MarkedAsyncCallback<>(new AsyncCallback<Void>() {
116 118
@Override
117 119
public void onSuccess(Void result) {
118 120
Notification.notify(stringMessages.successfullyUpdatedResultImportUrls(),
... ...
@@ -124,7 +126,7 @@ public class ResultImportUrlsListComposite extends Composite {
124 126
errorReporter
125 127
.reportError(stringMessages.errorAddingResultImportUrl(caught.getMessage()));
126 128
}
127
- });
129
+ }));
128 130
}
129 131
@Override
130 132
public void cancel() {
java/com.sap.sse.replication.interfaces/src/com/sap/sse/replication/FullyInitializedReplicableTracker.java
... ...
@@ -130,7 +130,8 @@ public class FullyInitializedReplicableTracker<R extends Replicable<?, ?>> exten
130 130
* {@link #waitForService(long)}). If no such service object can be found before timing out, {@code null}
131 131
* is returned. Once a service object has been retrieved and a non-{@code null} {@link #replicationServiceTracker}
132 132
* has been provided at construction time, the {@link ReplicationService} is obtained from that tracker by
133
- * waiting for it at least {@code timeoutInMillis} milliseconds and then
133
+ * waiting for it at least {@code timeoutInMillis} milliseconds and then is asked to wait for the replication
134
+ * to be fully initialized, so in particular having received and incorporated the initial load.
134 135
*
135 136
* @param timeoutInMillis
136 137
* 0 means indefinite wait time
java/com.sap.sse.security/src/com/sap/sse/security/SecurityInitializationCustomizer.java
... ...
@@ -1,6 +1,6 @@
1 1
package com.sap.sse.security;
2 2
3
+@FunctionalInterface
3 4
public interface SecurityInitializationCustomizer {
4
-
5 5
void customizeSecurityService(SecurityService securityService);
6 6
}
java/com.sap.sse.security/src/com/sap/sse/security/impl/Activator.java
... ...
@@ -19,7 +19,6 @@ import com.sap.sse.ServerInfo;
19 19
import com.sap.sse.classloading.ServiceTrackerCustomizerForClassLoaderSupplierRegistrations;
20 20
import com.sap.sse.mail.MailService;
21 21
import com.sap.sse.replication.Replicable;
22
-import com.sap.sse.replication.ReplicationMasterDescriptor;
23 22
import com.sap.sse.replication.ReplicationService;
24 23
import com.sap.sse.rest.CORSFilterConfiguration;
25 24
import com.sap.sse.security.SecurityInitializationCustomizer;
... ...
@@ -289,9 +288,12 @@ public class Activator implements BundleActivator {
289 288
// create security service, it will also create a default admin user if no users exist
290 289
createAndRegisterSecurityService(bundleContext, userStore, accessControlStore, subscriptionPlanProvider);
291 290
applyCustomizations();
292
- migrate(userStore, securityService.get());
293
- final ReplicationMasterDescriptor masterDescriptor = securityService.get().getMasterDescriptor();
294
- if (masterDescriptor == null) {
291
+ final ReplicationService replicationService = ServiceTrackerFactory.createAndOpen(context, ReplicationService.class).waitForService(0);
292
+ // See also bug 6244: if the SecurityService will become a replica, don't worry about subscriptions
293
+ // and migrations as that is relevant only on the primary, and we don't want to establish or even replicate
294
+ // effects of temporary locally-created SERVER objects, ownerships, and permissions.
295
+ if (!replicationService.isReplicationStarting() && securityService.get().getMasterDescriptor() == null) {
296
+ migrate(userStore, securityService.get());
295 297
startSubscriptionDataUpdateTask(bundleContext);
296 298
startSubscriptionPlanUpdateTask(bundleContext);
297 299
}