Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
2 changes: 0 additions & 2 deletions api/.env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,11 @@ SMTP_CLIENT_SENDER_NAME=From Name
SMTP_CLIENT_REPORT=

OTP_EXPIRATION=15m
SUBSCRIPTION_TYPE=
MAX_CREDENTIALS=10
MAX_RECIPIENTS=10
MAX_DAILY_ALIASES=100
MAX_DAILY_SEND_REPLY=100
MAX_SESSIONS=10
ACCOUNT_GRACE_PERIOD_DAYS=194
ID_LIMITER_MAX=5
ID_LIMITER_EXPIRATION=60m

Expand Down
2 changes: 1 addition & 1 deletion api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64

RUN go build -o api cmd/main.go
RUN go build -o api ./cmd/

# stage 2: copy only the application binary file and necessary files to the alpine container
FROM alpine:latest AS production
Expand Down
17 changes: 17 additions & 0 deletions api/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"log"
"os"

"ivpn.net/email/api/config"
"ivpn.net/email/api/internal/cron"
Expand Down Expand Up @@ -39,6 +40,22 @@ func Run() error {
}

func main() {
if len(os.Args) > 1 && os.Args[1] == "send-template-managed" {
if err := runSendTemplateManaged(os.Args[2:]); err != nil {
log.Println(err)
os.Exit(1)
}
return
}

if len(os.Args) > 1 && os.Args[1] == "send-template" {
if err := runSendTemplate(os.Args[2:]); err != nil {
log.Println(err)
os.Exit(1)
}
return
}

err := Run()
if err != nil {
log.Println(err)
Expand Down
142 changes: 142 additions & 0 deletions api/cmd/send_template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package main

import (
"context"
"flag"
"fmt"
"log"
"os"
"strings"

"ivpn.net/email/api/config"
"ivpn.net/email/api/internal/client/mailer"
"ivpn.net/email/api/internal/repository"
)

func smtpConfigFromEnv() config.SMTPClientConfig {
return config.SMTPClientConfig{
Host: os.Getenv("SMTP_CLIENT_HOST"),
Port: os.Getenv("SMTP_CLIENT_PORT"),
User: os.Getenv("SMTP_CLIENT_USER"),
Password: os.Getenv("SMTP_CLIENT_PASSWORD"),
Sender: os.Getenv("SMTP_CLIENT_SENDER"),
SenderName: os.Getenv("SMTP_CLIENT_SENDER_NAME"),
Report: os.Getenv("SMTP_CLIENT_REPORT"),
TokenSecret: os.Getenv("TOKEN_SECRET"),
}
}

func dbConfigFromEnv() config.DBConfig {
return config.DBConfig{
Hosts: strings.Split(os.Getenv("DB_HOSTS"), ","),
Port: os.Getenv("DB_PORT"),
Name: os.Getenv("DB_NAME"),
User: os.Getenv("DB_USER"),
Password: os.Getenv("DB_PASSWORD"),
}
}

func runSendTemplateManaged(args []string) error {
fs := flag.NewFlagSet("send-template-managed", flag.ContinueOnError)
tmpl := fs.String("template", "", "Template filename (e.g. expiring_beta.tmpl)")
subject := fs.String("subject", "", "Email subject")

if err := fs.Parse(args); err != nil {
return err
}

if *tmpl == "" {
fs.Usage()
return fmt.Errorf("--template is required")
}
if *subject == "" {
fs.Usage()
return fmt.Errorf("--subject is required")
}

db, err := repository.NewDB(dbConfigFromEnv())
if err != nil {
return fmt.Errorf("connecting to database: %w", err)
}

emails, err := db.GetManagedUserEmails(context.Background())
if err != nil {
return fmt.Errorf("querying managed users: %w", err)
}

if len(emails) == 0 {
log.Println("no managed users found")
return nil
}

log.Printf("found %d managed user(s)", len(emails))

m := mailer.New(smtpConfigFromEnv())

var failed int
for _, email := range emails {
err := m.SendTemplate(email, *subject, *tmpl, nil)
if err != nil {
log.Printf("failed to send to %s: %s", email, err.Error())
failed++
} else {
log.Printf("sent to %s", email)
}
}

if failed > 0 {
return fmt.Errorf("%d recipient(s) failed", failed)
}
return nil
}

func runSendTemplate(args []string) error {
fs := flag.NewFlagSet("send-template", flag.ContinueOnError)
tmpl := fs.String("template", "", "Template filename (e.g. expiring_beta.tmpl)")
to := fs.String("to", "", "Comma-separated list of recipient email addresses")
subject := fs.String("subject", "", "Email subject")

if err := fs.Parse(args); err != nil {
return err
}

if *tmpl == "" {
fs.Usage()
return fmt.Errorf("--template is required")
}
if *to == "" {
fs.Usage()
return fmt.Errorf("--to is required")
}
if *subject == "" {
fs.Usage()
return fmt.Errorf("--subject is required")
}

recipients := strings.Split(*to, ",")
for i, r := range recipients {
recipients[i] = strings.TrimSpace(r)
}

cfg := smtpConfigFromEnv()
m := mailer.New(cfg)

var failed int
for _, email := range recipients {
if email == "" {
continue
}
err := m.SendTemplate(email, *subject, *tmpl, nil)
if err != nil {
log.Printf("failed to send to %s: %s", email, err.Error())
failed++
} else {
log.Printf("sent to %s", email)
}
}

if failed > 0 {
return fmt.Errorf("%d recipient(s) failed", failed)
}
return nil
}
41 changes: 16 additions & 25 deletions api/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,14 @@ type SMTPClientConfig struct {
}

type ServiceConfig struct {
OTPExpiration time.Duration
SubscriptionType string
MaxCredentials int
MaxRecipients int
MaxDailyAliases int
MaxDailySendReply int
MaxSessions int
AccountGracePeriodDays int
IdLimiterMax int
IdLimiterExpiration time.Duration
OTPExpiration time.Duration
MaxCredentials int
MaxRecipients int
MaxDailyAliases int
MaxDailySendReply int
MaxSessions int
IdLimiterMax int
IdLimiterExpiration time.Duration
}

type Config struct {
Expand Down Expand Up @@ -138,11 +136,6 @@ func New() (Config, error) {
return Config{}, err
}

accountGracePeriodDays, err := strconv.Atoi(os.Getenv("ACCOUNT_GRACE_PERIOD_DAYS"))
if err != nil {
return Config{}, err
}

dbHosts := strings.Split(os.Getenv("DB_HOSTS"), ",")
redisAddrs := strings.Split(os.Getenv("REDIS_ADDRESSES"), ",")
apiTrustedProxies := strings.Split(os.Getenv("API_TRUSTED_PROXIES"), ",")
Expand Down Expand Up @@ -209,16 +202,14 @@ func New() (Config, error) {
},

Service: ServiceConfig{
OTPExpiration: otpExp,
SubscriptionType: os.Getenv("SUBSCRIPTION_TYPE"),
MaxCredentials: maxCredentials,
MaxRecipients: maxRecipients,
MaxDailyAliases: maxDailyAliases,
MaxDailySendReply: maxDailySendReply,
MaxSessions: maxSessions,
AccountGracePeriodDays: accountGracePeriodDays,
IdLimiterMax: idLimiterMax,
IdLimiterExpiration: idLimiterExpiration,
OTPExpiration: otpExp,
MaxCredentials: maxCredentials,
MaxRecipients: maxRecipients,
MaxDailyAliases: maxDailyAliases,
MaxDailySendReply: maxDailySendReply,
MaxSessions: maxSessions,
IdLimiterMax: idLimiterMax,
IdLimiterExpiration: idLimiterExpiration,
},
}, nil
}
36 changes: 36 additions & 0 deletions api/internal/client/mailer/templates/expiring_beta.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{{define "body"}}
Hi,

The Mailx beta period ends on May 19. An active IVPN Plus or Pro Suite subscription is required to continue using the service.

Sync your IVPN account with your Mailx account using the link below:

https://www.ivpn.net/en/account/?action=sync&service=mail

This takes you to your IVPN Account page. Under "Additional Services", click the resync button next to Mailx. The process takes about 3 minutes.

Complete this before May 19 to avoid service restrictions.

Note: Your IVPN account ID is not shared with Mailx. Details: https://www.ivpn.net/unlinked-access/

If you also signed up for the modDNS beta, you will receive a separate email with instructions. Each service must be synced separately.

Any issues? Contact us: mailx@ivpn.net

Sent by Mailx
{{end}}

{{define "bodyHtml"}}
<div style="font-family: Arial, Helvetica, sans-serif;font-size: 15px;">
Hi,<br><br>
The Mailx beta period ends on May 19. An active IVPN Plus or Pro Suite subscription is required to continue using the service.<br><br>
Sync your IVPN account with your Mailx account using the link below:<br><br>
<a href="https://www.ivpn.net/en/account/?action=sync&service=mail">https://www.ivpn.net/en/account/?action=sync&service=mail</a><br><br>
This takes you to your IVPN Account page. Under "Additional Services", click the resync button next to Mailx. The process takes about 3 minutes.<br><br>
Complete this before May 19 to avoid service restrictions.<br><br>
Note: Your IVPN account ID is not shared with Mailx. Details: <a href="https://www.ivpn.net/unlinked-access/">https://www.ivpn.net/unlinked-access/</a><br><br>
If you also signed up for the modDNS beta, you will receive a separate email with instructions. Each service must be synced separately.<br><br>
Any issues? Contact us: <a href="mailto:mailx@ivpn.net">mailx@ivpn.net</a><br><br>
Sent by Mailx
</div>
{{end}}
8 changes: 4 additions & 4 deletions api/internal/client/mailer/templates/expiring_sub.tmpl
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{{define "body"}}
Hello,

Your {{.from}} account is in limited access mode.
Your {{.from}} account is in Limited Access mode.

You cannot create new aliases or recipients, reply to forwards or send emails via {{.from}}.

Incoming forwards are still processed for 14 days from the date of this message.
Existing aliases forward normally with no time limit.

To regain full access with no restrictions, add time to your IVPN account.

Expand All @@ -15,9 +15,9 @@ Sent by {{.from}}
{{define "bodyHtml"}}
<div style="font-family: Arial, Helvetica, sans-serif;font-size: 15px;">
Hello,<br><br>
Your {{.from}} account is in limited access mode.<br><br>
Your {{.from}} account is in Limited Access mode.<br><br>
You cannot create new aliases or recipients, reply to forwards or send emails via {{.from}}.<br><br>
Incoming forwards are still processed for 14 days from the date of this message.<br><br>
Existing aliases forward normally with no time limit.<br><br>
To regain full access with no restrictions, add time to your IVPN account.<br><br>
Sent by {{.from}}
</div>
Expand Down
16 changes: 5 additions & 11 deletions api/internal/cron/cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ func New(db *gorm.DB) {
return
}

err = gocron.Every(1).Hour().Do(jobs.DeleteUnverifiedUsers, db)
if err != nil {
log.Println("Error scheduling job:", err)
return
}
// err = gocron.Every(1).Hour().Do(jobs.DeleteUnverifiedUsers, db)
// if err != nil {
// log.Println("Error scheduling job:", err)
// return
// }

err = gocron.Every(1).Hour().Do(jobs.CleanupDeletedAliases, db)
if err != nil {
Expand All @@ -46,12 +46,6 @@ func New(db *gorm.DB) {
return
}

err = gocron.Every(1).Hour().Do(jobs.DeleteExpiredUsers, db, cfg.Service)
if err != nil {
log.Println("Error scheduling job:", err)
return
}

err = gocron.Every(1).Hour().Do(jobs.NotifyExpiringSubscriptionsJob, cfg, db)
if err != nil {
log.Println("Error scheduling job:", err)
Expand Down
6 changes: 3 additions & 3 deletions api/internal/cron/jobs/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@ func NotifyExpiringSubscriptionsJob(cfg config.Config, db *gorm.DB) {
// Set `notified` to false for all subscriptions that are active
func UpdateActiveSubscriptions(db *gorm.DB) {
err := db.Model(&model.Subscription{}).
Where("active_until >= NOW()").
Where("updated_at >= NOW() - INTERVAL 3 DAY AND active_until >= NOW() - INTERVAL 3 DAY AND tier != ?", model.Tier1).
UpdateColumn("notified", false).Error
if err != nil {
log.Println("Error resetting notified flag for active subscriptions:", err)
}
}

// Find subscriptions with `notified` false and `active_until` expired 1 day ago
// Find subscriptions with `notified` false and in limited access (updated_at or active_until expired 3 days ago)
func GetExpiringSubscriptions(db *gorm.DB) ([]model.Subscription, error) {
subs := []model.Subscription{}
err := db.Where("notified = false AND active_until < NOW() - INTERVAL 1 DAY").Find(&subs).Error
err := db.Where("notified = false AND (updated_at < NOW() - INTERVAL 3 DAY OR active_until < NOW() - INTERVAL 3 DAY OR tier = ?)", model.Tier1).Find(&subs).Error
if err != nil {
log.Println("Error fetching expiring subscriptions:", err)
return nil, err
Expand Down
Loading
Loading