Merge branch 'main' into bug6242

This commit is contained in:
masha.kashirina
2026-05-01 15:04:06 +02:00
8 changed files with 272 additions and 9 deletions
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# Usage: ${0} {MONGO-URI} {MONTHS}
# Example: findUsersWithSessionInMonths.sh "mongodb://localhost/winddb?replicaSet=rs0" 12
echo '
var cutoff = new Date();
cutoff.setMonth(cutoff.getMonth() - '${2}');
var activeUsers = db.SESSIONS.distinct(
"SESSION_ATTRIBUTES.SESSION_ATTRIBUTE_VALUE.SESSION_PRINCIPAL_REALM_VALUE",
{ SESSION_START_TIMESTAMP: { $gte: cutoff } }
).flat();
db.USERS.find(
{
EMAIL_VALIDATED: true,
EMAIL: { $exists: true, $ne: "" },
NAME: { $in: activeUsers },
$or: [
{ DID_OPT_OUT_OF_FEATURE_AND_COMMUNITY_EMAILS: { $exists: false } },
{ DID_OPT_OUT_OF_FEATURE_AND_COMMUNITY_EMAILS: false }
]
},
{ EMAIL: 1, FULLNAME: 1, NAME: 1, _id: 0 }
).forEach(function(u) {
var name = (u.FULLNAME && u.FULLNAME.trim()) || u.NAME || "user";
print(u.EMAIL + "," + name);
})' | mongosh "${1}"
+89
View File
@@ -0,0 +1,89 @@
# sendEmails.sh — Personalized Email Sender
Bash script for sending personalized HTML emails via SMTP (AWS WorkMail).
## Prerequisites
- `curl` with SMTP/TLS support
- `base64` (coreutils)
- SMTP credentials for the WorkMail account (`support@sapsailing.com`)
## Files
| File | Purpose |
|---|---|
| `sendEmails.sh` | The sending script |
| `emailTemplate.html` | HTML email template with `{{NAME}}` placeholder |
| `recipients.csv` | CSV file with recipient list |
## CSV Format
```csv
email,name
alice@example.com,Alice
bob@example.com,Bob
sailor42@example.com,sailor42
```
- **email** — recipient address
- **name** — inserted into the salutation ("Dear {{NAME}},"). Use the user's first name if known, otherwise their username/nickname.
- The header row is auto-detected and skipped if the first field starts with `email` (case-insensitive).
## Usage
```bash
./sendEmails.sh [--dry-run] <recipients.csv> [template.html]
```
| Argument | Required | Default | Description |
|---|---|---|---|
| `--dry-run` | no | — | Preview personalized emails without sending |
| `recipients.csv` | yes | — | Path to the CSV file |
| `template.html` | no | `/tmp/emailTemplate.html` | Path to the HTML template |
## Environment Variables
| Variable | Required | Description |
|---|---|---|
| `SMTP_USER` | yes (unless `--dry-run`) | SMTP username for WorkMail |
| `SMTP_PASS` | yes (unless `--dry-run`) | SMTP password for WorkMail |
## Examples
Preview all emails without sending:
```bash
./sendEmails.sh --dry-run recipients.csv
```
Send using a custom template:
```bash
export SMTP_USER="your-smtp-username"
export SMTP_PASS="your-smtp-password"
./sendEmails.sh recipients.csv myTemplate.html
```
## Configuration
These values are set at the top of the script and can be adjusted:
| Variable | Default | Description |
|---|---|---|
| `FROM_ADDR` | `SAP Sailing Analytics <support@sapsailing.com>` | Sender display name and address |
| `SUBJECT` | `Your SAP Sailing Analytics Account — Open Source Transition Update` | Email subject line |
| `SMTP_URL` | `smtps://smtp.mail.eu-west-1.awsapps.com:465` | WorkMail SMTP endpoint |
## How It Works
1. Reads the CSV line by line, skipping the header row.
2. For each recipient, replaces `{{NAME}}` in the HTML template with the name from the CSV.
3. Constructs a MIME message with UTF-8 base64-encoded subject and body.
4. Sends via `curl` over SMTPS to the WorkMail endpoint.
5. Waits 1 second between sends to respect SES rate limits.
## Recommended Workflow
1. **Preview** — Run with `--dry-run` and review the output.
2. **Test** — Send to your own address first (`echo "email,name" > test.csv && echo "you@example.com,YourName" >> test.csv`).
3. **Send** — Run against the full recipient list.
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env bash
set -euo pipefail
FROM_ADDR="SAP Sailing Analytics <support@sapsailing.com>"
SUBJECT="SAP Sailing Analytics — Open Source Transition Update"
SMTP_URL="smtps://smtp.mail.eu-west-1.awsapps.com:465"
usage() {
cat <<'EOF'
Usage: sendEmails.sh [--dry-run] <recipients.csv> [template.html]
recipients.csv CSV file with columns: email,name
template.html HTML template with {{NAME}} placeholder
(default: /tmp/emailTemplate.html)
--dry-run Print personalized emails to stdout without sending
Environment variables:
SMTP_USER SMTP username (required unless --dry-run)
SMTP_PASS SMTP password (required unless --dry-run)
CSV format example:
email,name
alice@example.com,Alice
bob@example.com,Bob
EOF
exit 1
}
DRY_RUN=false
if [[ "${1:-}" == "--dry-run" ]]; then
DRY_RUN=true
shift
fi
CSV="${1:-}"
TEMPLATE="${2:-/tmp/emailTemplate.html}"
[[ -z "$CSV" ]] && usage
[[ ! -f "$CSV" ]] && { echo "Error: CSV file not found: $CSV"; exit 1; }
[[ ! -f "$TEMPLATE" ]] && { echo "Error: Template file not found: $TEMPLATE"; exit 1; }
if [[ "$DRY_RUN" == false ]]; then
[[ -z "${SMTP_USER:-}" ]] && { echo "Error: SMTP_USER not set"; exit 1; }
[[ -z "${SMTP_PASS:-}" ]] && { echo "Error: SMTP_PASS not set"; exit 1; }
fi
TEMPLATE_BODY=$(cat "$TEMPLATE")
SENT=0
FAILED=0
SKIPPED_HEADER=false
while IFS= read -r line || [[ -n "$line" ]]; do
# Skip empty lines
[[ -z "${line// }" ]] && continue
# Skip header row
if [[ "$SKIPPED_HEADER" == false ]]; then
SKIPPED_HEADER=true
if echo "$line" | grep -qi "^email"; then
continue
fi
fi
EMAIL=$(echo "$line" | cut -d',' -f1 | xargs)
NAME=$(echo "$line" | cut -d',' -f2- | xargs)
[[ -z "$EMAIL" ]] && continue
BODY="${TEMPLATE_BODY//\{\{NAME\}\}/$NAME}"
if [[ "$DRY_RUN" == true ]]; then
echo "========================================"
echo "To: $EMAIL"
echo "From: $FROM_ADDR"
echo "Subject: $SUBJECT"
echo "----------------------------------------"
echo "$BODY"
echo ""
SENT=$((SENT + 1))
continue
fi
MIME_MSG=$(cat <<MIME
From: $FROM_ADDR
To: $EMAIL
Subject: =?UTF-8?B?$(echo -n "$SUBJECT" | base64 -w0)?=
MIME-Version: 1.0
Content-Type: text/html; charset=UTF-8
Content-Transfer-Encoding: base64
$(echo -n "$BODY" | base64 -w76)
MIME
)
RETRIES=0
MAX_RETRIES=3
SEND_OK=false
while [[ $RETRIES -le $MAX_RETRIES ]]; do
if echo "$MIME_MSG" | curl --silent --show-error \
--url "$SMTP_URL" \
--ssl-reqd \
--mail-from "support@sapsailing.com" \
--mail-rcpt "$EMAIL" \
--user "${SMTP_USER}:${SMTP_PASS}" \
--upload-file - 2>/tmp/curl_err.txt; then
echo "Sent to $EMAIL ($NAME)"
SENT=$((SENT + 1))
SEND_OK=true
break
else
if grep -q "421" /tmp/curl_err.txt 2>/dev/null; then
RETRIES=$((RETRIES + 1))
WAIT=$((RETRIES * 5))
echo " Rate limited, retrying in ${WAIT}s... (attempt $((RETRIES))/$MAX_RETRIES)" >&2
sleep "$WAIT"
else
break
fi
fi
done
if [[ "$SEND_OK" == false ]]; then
echo "FAILED: $EMAIL ($NAME)" >&2
FAILED=$((FAILED + 1))
fi
sleep 3
done < "$CSV"
echo ""
if [[ "$DRY_RUN" == true ]]; then
echo "Dry run complete. $SENT emails previewed."
else
echo "Done. Sent: $SENT, Failed: $FAILED"
fi
@@ -15,6 +15,7 @@ import java.util.logging.Logger;
import org.apache.http.client.ClientProtocolException;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.UnavailableSecurityManagerException;
import org.apache.shiro.authz.AuthorizationException;
import org.json.simple.parser.ParseException;
import org.osgi.util.tracker.ServiceTracker;
@@ -312,7 +313,13 @@ public class AIAgentImpl implements AIAgent {
for (final Leaderboard leaderboard : event.getLeaderboards()) {
addNewRaceColumnListenerToLeaderboard(leaderboard);
}
logger.info("User "+SecurityUtils.getSubject().getPrincipal()+" activated AI comments for event "+event.getName()+" with ID "+event.getId());
Object principal;
try {
principal = SecurityUtils.getSubject().getPrincipal();
} catch (UnavailableSecurityManagerException e) {
principal = "null";
}
logger.info("User "+principal+" activated AI comments for event "+event.getName()+" with ID "+event.getId());
listeners.forEach(l->l.startedCommentingOnEvent(event));
}
@@ -23,6 +23,7 @@ import com.sap.sse.common.Util.Pair;
import com.sap.sse.gwt.client.ErrorReporter;
import com.sap.sse.gwt.client.Notification;
import com.sap.sse.gwt.client.Notification.NotificationType;
import com.sap.sse.gwt.client.async.MarkedAsyncCallback;
import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel;
import com.sap.sse.gwt.client.dialog.DataEntryDialog;
import com.sap.sse.security.ui.client.component.AccessControlledButtonPanel;
@@ -112,7 +113,8 @@ public class ResultImportUrlsListComposite extends Composite {
new DataEntryDialog.DialogCallback<UrlDTO>() {
@Override
public void ok(UrlDTO url) {
sailingServiceWrite.addResultImportUrl(getSelectedProviderName(), url, new AsyncCallback<Void>() {
sailingServiceWrite.addResultImportUrl(getSelectedProviderName(), url,
new MarkedAsyncCallback<>(new AsyncCallback<Void>() {
@Override
public void onSuccess(Void result) {
Notification.notify(stringMessages.successfullyUpdatedResultImportUrls(),
@@ -124,7 +126,7 @@ public class ResultImportUrlsListComposite extends Composite {
errorReporter
.reportError(stringMessages.errorAddingResultImportUrl(caught.getMessage()));
}
});
}));
}
@Override
public void cancel() {
@@ -130,7 +130,8 @@ public class FullyInitializedReplicableTracker<R extends Replicable<?, ?>> exten
* {@link #waitForService(long)}). If no such service object can be found before timing out, {@code null}
* is returned. Once a service object has been retrieved and a non-{@code null} {@link #replicationServiceTracker}
* has been provided at construction time, the {@link ReplicationService} is obtained from that tracker by
* waiting for it at least {@code timeoutInMillis} milliseconds and then
* waiting for it at least {@code timeoutInMillis} milliseconds and then is asked to wait for the replication
* to be fully initialized, so in particular having received and incorporated the initial load.
*
* @param timeoutInMillis
* 0 means indefinite wait time
@@ -1,6 +1,6 @@
package com.sap.sse.security;
@FunctionalInterface
public interface SecurityInitializationCustomizer {
void customizeSecurityService(SecurityService securityService);
}
@@ -19,7 +19,6 @@ import com.sap.sse.ServerInfo;
import com.sap.sse.classloading.ServiceTrackerCustomizerForClassLoaderSupplierRegistrations;
import com.sap.sse.mail.MailService;
import com.sap.sse.replication.Replicable;
import com.sap.sse.replication.ReplicationMasterDescriptor;
import com.sap.sse.replication.ReplicationService;
import com.sap.sse.rest.CORSFilterConfiguration;
import com.sap.sse.security.SecurityInitializationCustomizer;
@@ -289,9 +288,12 @@ public class Activator implements BundleActivator {
// create security service, it will also create a default admin user if no users exist
createAndRegisterSecurityService(bundleContext, userStore, accessControlStore, subscriptionPlanProvider);
applyCustomizations();
migrate(userStore, securityService.get());
final ReplicationMasterDescriptor masterDescriptor = securityService.get().getMasterDescriptor();
if (masterDescriptor == null) {
final ReplicationService replicationService = ServiceTrackerFactory.createAndOpen(context, ReplicationService.class).waitForService(0);
// See also bug 6244: if the SecurityService will become a replica, don't worry about subscriptions
// and migrations as that is relevant only on the primary, and we don't want to establish or even replicate
// effects of temporary locally-created SERVER objects, ownerships, and permissions.
if (!replicationService.isReplicationStarting() && securityService.get().getMasterDescriptor() == null) {
migrate(userStore, securityService.get());
startSubscriptionDataUpdateTask(bundleContext);
startSubscriptionPlanUpdateTask(bundleContext);
}