mirror of
https://github.com/eclipse-sailing-analytics/sailing-analytics.git
synced 2026-09-16 10:48:47 +00:00
Merge branch 'main' into bug6242
This commit is contained in:
+27
@@ -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}"
|
||||
@@ -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.
|
||||
Executable
+135
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -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() {
|
||||
|
||||
+2
-1
@@ -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
-1
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user