- Add contracts_enabled field to SubscriptionPlan model
- Add contracts toggle to plan create/edit modal in platform settings
- Hide contracts menu item for tenants without contracts permission
- Protect /contracts routes with canUse('contracts') check
- Add HasContractsPermission to contracts API ViewSets
- Add contracts to PlanPermissions interface and feature definitions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
729 lines
22 KiB
Python
729 lines
22 KiB
Python
"""
|
|
Platform Admin Models
|
|
Models for platform-level operations like tenant invitations
|
|
"""
|
|
import secrets
|
|
from datetime import timedelta
|
|
from django.db import models
|
|
from django.utils import timezone
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.core.cache import cache
|
|
|
|
|
|
class PlatformSettings(models.Model):
|
|
"""
|
|
Singleton model for platform-wide settings.
|
|
Settings like Stripe keys can be overridden via environment variables.
|
|
"""
|
|
|
|
# Stripe settings (can be overridden or set via admin)
|
|
stripe_secret_key = models.CharField(
|
|
max_length=255,
|
|
blank=True,
|
|
default='',
|
|
help_text="Stripe secret key (overrides env if set)"
|
|
)
|
|
stripe_publishable_key = models.CharField(
|
|
max_length=255,
|
|
blank=True,
|
|
default='',
|
|
help_text="Stripe publishable key (overrides env if set)"
|
|
)
|
|
stripe_webhook_secret = models.CharField(
|
|
max_length=255,
|
|
blank=True,
|
|
default='',
|
|
help_text="Stripe webhook secret (overrides env if set)"
|
|
)
|
|
|
|
# Stripe validation info
|
|
stripe_account_id = models.CharField(
|
|
max_length=255,
|
|
blank=True,
|
|
default='',
|
|
help_text="Stripe account ID (set after validation)"
|
|
)
|
|
stripe_account_name = models.CharField(
|
|
max_length=255,
|
|
blank=True,
|
|
default='',
|
|
help_text="Stripe account name (set after validation)"
|
|
)
|
|
stripe_keys_validated_at = models.DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
help_text="When the Stripe keys were last validated"
|
|
)
|
|
stripe_validation_error = models.TextField(
|
|
blank=True,
|
|
default='',
|
|
help_text="Last validation error, if any"
|
|
)
|
|
|
|
# OAuth settings - stored as JSON for flexibility
|
|
oauth_settings = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text="OAuth provider settings (Google, Apple, etc.)"
|
|
)
|
|
# Example oauth_settings structure:
|
|
# {
|
|
# "allow_registration": true,
|
|
# "google": {"enabled": true, "client_id": "...", "client_secret": "..."},
|
|
# "apple": {"enabled": false, "client_id": "", ...},
|
|
# ...
|
|
# }
|
|
|
|
# Email settings
|
|
email_check_interval_minutes = models.PositiveIntegerField(
|
|
default=5,
|
|
help_text="How often to check for new incoming emails (in minutes)"
|
|
)
|
|
|
|
# Timestamps
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
app_label = 'platform_admin'
|
|
verbose_name = 'Platform Settings'
|
|
verbose_name_plural = 'Platform Settings'
|
|
|
|
def __str__(self):
|
|
return "Platform Settings"
|
|
|
|
def save(self, *args, **kwargs):
|
|
# Ensure only one instance exists
|
|
self.pk = 1
|
|
super().save(*args, **kwargs)
|
|
# Clear cache
|
|
cache.delete('platform_settings')
|
|
|
|
def delete(self, *args, **kwargs):
|
|
# Prevent deletion
|
|
pass
|
|
|
|
@classmethod
|
|
def get_instance(cls):
|
|
"""Get or create the singleton instance"""
|
|
# Try cache first
|
|
cached = cache.get('platform_settings')
|
|
if cached:
|
|
return cached
|
|
|
|
instance, _ = cls.objects.get_or_create(pk=1)
|
|
cache.set('platform_settings', instance, timeout=300) # 5 min cache
|
|
return instance
|
|
|
|
def get_stripe_secret_key(self):
|
|
"""Get Stripe secret key (database or env)"""
|
|
from django.conf import settings
|
|
if self.stripe_secret_key:
|
|
return self.stripe_secret_key
|
|
return getattr(settings, 'STRIPE_SECRET_KEY', '')
|
|
|
|
def get_stripe_publishable_key(self):
|
|
"""Get Stripe publishable key (database or env)"""
|
|
from django.conf import settings
|
|
if self.stripe_publishable_key:
|
|
return self.stripe_publishable_key
|
|
return getattr(settings, 'STRIPE_PUBLISHABLE_KEY', '')
|
|
|
|
def get_stripe_webhook_secret(self):
|
|
"""Get Stripe webhook secret (database or env)"""
|
|
from django.conf import settings
|
|
if self.stripe_webhook_secret:
|
|
return self.stripe_webhook_secret
|
|
return getattr(settings, 'STRIPE_WEBHOOK_SECRET', '')
|
|
|
|
def has_stripe_keys(self):
|
|
"""Check if Stripe keys are configured"""
|
|
return bool(self.get_stripe_secret_key() and self.get_stripe_publishable_key())
|
|
|
|
def stripe_keys_from_env(self):
|
|
"""Check if Stripe keys come from environment"""
|
|
from django.conf import settings
|
|
return (
|
|
not self.stripe_secret_key and
|
|
bool(getattr(settings, 'STRIPE_SECRET_KEY', ''))
|
|
)
|
|
|
|
def mask_key(self, key, visible_chars=8):
|
|
"""Mask a key, showing only first/last chars"""
|
|
if not key:
|
|
return ''
|
|
if len(key) <= visible_chars * 2:
|
|
return '*' * len(key)
|
|
return f"{key[:visible_chars]}...{key[-4:]}"
|
|
|
|
|
|
class SubscriptionPlan(models.Model):
|
|
"""
|
|
Subscription plans that can be assigned to tenants.
|
|
Links to Stripe products/prices for billing.
|
|
"""
|
|
|
|
class PlanType(models.TextChoices):
|
|
BASE = 'base', _('Base Plan')
|
|
ADDON = 'addon', _('Add-on')
|
|
|
|
name = models.CharField(max_length=100)
|
|
description = models.TextField(blank=True, default='')
|
|
plan_type = models.CharField(
|
|
max_length=10,
|
|
choices=PlanType.choices,
|
|
default=PlanType.BASE
|
|
)
|
|
|
|
# Stripe integration
|
|
stripe_product_id = models.CharField(
|
|
max_length=255,
|
|
blank=True,
|
|
default='',
|
|
help_text="Stripe Product ID"
|
|
)
|
|
stripe_price_id = models.CharField(
|
|
max_length=255,
|
|
blank=True,
|
|
default='',
|
|
help_text="Stripe Price ID (for monthly billing)"
|
|
)
|
|
|
|
# Pricing
|
|
price_monthly = models.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=2,
|
|
null=True,
|
|
blank=True,
|
|
help_text="Monthly price in dollars"
|
|
)
|
|
price_yearly = models.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=2,
|
|
null=True,
|
|
blank=True,
|
|
help_text="Yearly price in dollars"
|
|
)
|
|
|
|
# Business tier this plan corresponds to (empty for addons)
|
|
business_tier = models.CharField(
|
|
max_length=50,
|
|
choices=[
|
|
('', 'N/A (Add-on)'),
|
|
('Free', 'Free'),
|
|
('Starter', 'Starter'),
|
|
('Professional', 'Professional'),
|
|
('Business', 'Business'),
|
|
('Enterprise', 'Enterprise'),
|
|
],
|
|
blank=True,
|
|
default=''
|
|
)
|
|
|
|
# Features included (stored as JSON array of strings)
|
|
features = models.JSONField(
|
|
default=list,
|
|
blank=True,
|
|
help_text="List of feature descriptions"
|
|
)
|
|
|
|
# Platform permissions (what features this plan grants)
|
|
permissions = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text="Platform permissions to grant (e.g., can_accept_payments, can_use_custom_domain)"
|
|
)
|
|
|
|
# Feature limits (what capabilities this plan has)
|
|
limits = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text="Feature limits and capabilities to grant (e.g. max_users, max_resources)"
|
|
)
|
|
|
|
# Transaction fees for payment processing
|
|
transaction_fee_percent = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=2,
|
|
default=2.9,
|
|
help_text="Transaction fee percentage"
|
|
)
|
|
transaction_fee_fixed = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=2,
|
|
default=0.30,
|
|
help_text="Fixed transaction fee in dollars"
|
|
)
|
|
|
|
# SMS & Communication Settings
|
|
sms_enabled = models.BooleanField(
|
|
default=False,
|
|
help_text="Whether SMS reminders are available for this tier"
|
|
)
|
|
sms_price_per_message_cents = models.IntegerField(
|
|
default=3, # $0.03
|
|
help_text="Price per SMS message in cents"
|
|
)
|
|
|
|
# Masked Calling Settings
|
|
masked_calling_enabled = models.BooleanField(
|
|
default=False,
|
|
help_text="Whether masked calling is available for this tier"
|
|
)
|
|
masked_calling_price_per_minute_cents = models.IntegerField(
|
|
default=5, # $0.05
|
|
help_text="Price per voice minute in cents"
|
|
)
|
|
|
|
# Proxy Phone Number Settings
|
|
proxy_number_enabled = models.BooleanField(
|
|
default=False,
|
|
help_text="Whether tenants can have dedicated proxy numbers"
|
|
)
|
|
proxy_number_monthly_fee_cents = models.IntegerField(
|
|
default=200, # $2.00
|
|
help_text="Monthly fee per proxy number in cents"
|
|
)
|
|
|
|
# Contracts Feature
|
|
contracts_enabled = models.BooleanField(
|
|
default=False,
|
|
help_text="Whether tenants can use the contracts feature"
|
|
)
|
|
|
|
# Default Credit Settings (for new tenants on this tier)
|
|
default_auto_reload_enabled = models.BooleanField(
|
|
default=False,
|
|
help_text="Whether auto-reload is enabled by default for new tenants"
|
|
)
|
|
default_auto_reload_threshold_cents = models.IntegerField(
|
|
default=1000, # $10
|
|
help_text="Default auto-reload threshold in cents"
|
|
)
|
|
default_auto_reload_amount_cents = models.IntegerField(
|
|
default=2500, # $25
|
|
help_text="Default auto-reload amount in cents"
|
|
)
|
|
|
|
# Visibility
|
|
is_active = models.BooleanField(default=True)
|
|
is_public = models.BooleanField(
|
|
default=True,
|
|
help_text="Whether this plan is visible on public pricing page"
|
|
)
|
|
is_most_popular = models.BooleanField(
|
|
default=False,
|
|
help_text="Whether to highlight this plan as the most popular choice"
|
|
)
|
|
show_price = models.BooleanField(
|
|
default=True,
|
|
help_text="Whether to display the price on the marketing site (disable for 'Contact Us')"
|
|
)
|
|
|
|
# Timestamps
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
app_label = 'platform_admin'
|
|
ordering = ['price_monthly', 'name']
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.get_plan_type_display()})"
|
|
|
|
|
|
class TenantInvitation(models.Model):
|
|
"""
|
|
Invitation for new business owners to create their tenant.
|
|
Allows platform admins to pre-configure custom limits and permissions.
|
|
|
|
Flow:
|
|
1. Platform admin creates invitation with email and custom settings
|
|
2. System sends email with unique token link
|
|
3. Invitee clicks link, completes onboarding wizard
|
|
4. Tenant, domain, and owner user are created with pre-configured settings
|
|
"""
|
|
|
|
class Status(models.TextChoices):
|
|
PENDING = 'PENDING', _('Pending')
|
|
ACCEPTED = 'ACCEPTED', _('Accepted')
|
|
EXPIRED = 'EXPIRED', _('Expired')
|
|
CANCELLED = 'CANCELLED', _('Cancelled')
|
|
|
|
# Invitation target
|
|
email = models.EmailField(help_text="Email address to send invitation to")
|
|
token = models.CharField(max_length=64, unique=True)
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=Status.choices,
|
|
default=Status.PENDING
|
|
)
|
|
|
|
# Pre-configured business settings (owner can modify during onboarding)
|
|
suggested_business_name = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
help_text="Suggested business name (owner can change during onboarding)"
|
|
)
|
|
|
|
# Subscription settings
|
|
subscription_tier = models.CharField(
|
|
max_length=50,
|
|
choices=[
|
|
('FREE', 'Free Trial'),
|
|
('STARTER', 'Starter'),
|
|
('PROFESSIONAL', 'Professional'),
|
|
('ENTERPRISE', 'Enterprise'),
|
|
],
|
|
default='PROFESSIONAL'
|
|
)
|
|
|
|
# Custom limits (null = use tier defaults)
|
|
custom_max_users = models.IntegerField(
|
|
null=True,
|
|
blank=True,
|
|
help_text="Custom max users limit (null = use tier default)"
|
|
)
|
|
custom_max_resources = models.IntegerField(
|
|
null=True,
|
|
blank=True,
|
|
help_text="Custom max resources limit (null = use tier default)"
|
|
)
|
|
|
|
# Platform permissions (what features this tenant can access)
|
|
# These are special permissions not available in normal tier packages
|
|
permissions = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text="Platform permissions to grant (e.g., can_accept_payments, can_use_custom_domain)"
|
|
)
|
|
# Example permissions structure:
|
|
# {
|
|
# "can_manage_oauth_credentials": true,
|
|
# "can_accept_payments": true,
|
|
# "can_use_custom_domain": true,
|
|
# "can_white_label": false,
|
|
# "can_api_access": true,
|
|
# }
|
|
|
|
# Feature limits (what capabilities this tenant has)
|
|
limits = models.JSONField(
|
|
default=dict,
|
|
blank=True,
|
|
help_text="Feature limits and capabilities to grant"
|
|
)
|
|
# Example limits structure:
|
|
# {
|
|
# "can_add_video_conferencing": true,
|
|
# "max_event_types": 10, # null = unlimited
|
|
# "max_calendars_connected": 5, # null = unlimited
|
|
# "can_connect_to_api": true,
|
|
# "can_book_repeated_events": true,
|
|
# "can_require_2fa": true,
|
|
# "can_download_logs": false,
|
|
# "can_delete_data": false,
|
|
# "can_use_masked_phone_numbers": false,
|
|
# "can_use_pos": false,
|
|
# "can_use_mobile_app": true,
|
|
# }
|
|
|
|
# Personal message to include in email
|
|
personal_message = models.TextField(
|
|
blank=True,
|
|
help_text="Optional personal message to include in the invitation email"
|
|
)
|
|
|
|
# Metadata
|
|
invited_by = models.ForeignKey(
|
|
'users.User',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
related_name='tenant_invitations_sent',
|
|
help_text="Platform admin who sent the invitation"
|
|
)
|
|
|
|
# Timestamps
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
expires_at = models.DateTimeField()
|
|
accepted_at = models.DateTimeField(null=True, blank=True)
|
|
|
|
# Links to created resources (after acceptance)
|
|
created_tenant = models.ForeignKey(
|
|
'core.Tenant',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='invitation',
|
|
help_text="Tenant created when invitation was accepted"
|
|
)
|
|
created_user = models.ForeignKey(
|
|
'users.User',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='tenant_invitation_accepted',
|
|
help_text="Owner user created when invitation was accepted"
|
|
)
|
|
|
|
class Meta:
|
|
app_label = 'platform_admin'
|
|
ordering = ['-created_at']
|
|
indexes = [
|
|
models.Index(fields=['token']),
|
|
models.Index(fields=['email', 'status']),
|
|
models.Index(fields=['status', 'expires_at']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"Tenant invitation for {self.email} ({self.get_status_display()})"
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.token:
|
|
self.token = secrets.token_urlsafe(32)
|
|
if not self.expires_at:
|
|
# Default expiration: 7 days
|
|
self.expires_at = timezone.now() + timedelta(days=7)
|
|
super().save(*args, **kwargs)
|
|
|
|
def is_valid(self):
|
|
"""Check if invitation can still be accepted"""
|
|
if self.status != self.Status.PENDING:
|
|
return False
|
|
if timezone.now() > self.expires_at:
|
|
return False
|
|
return True
|
|
|
|
def accept(self, tenant, user):
|
|
"""Mark invitation as accepted and link to created resources"""
|
|
self.status = self.Status.ACCEPTED
|
|
self.accepted_at = timezone.now()
|
|
self.created_tenant = tenant
|
|
self.created_user = user
|
|
self.save()
|
|
|
|
def cancel(self):
|
|
"""Cancel a pending invitation"""
|
|
if self.status == self.Status.PENDING:
|
|
self.status = self.Status.CANCELLED
|
|
self.save()
|
|
|
|
def get_effective_max_users(self):
|
|
"""Get max users (custom or tier default)"""
|
|
if self.custom_max_users is not None:
|
|
return self.custom_max_users
|
|
# Tier defaults
|
|
tier_defaults = {
|
|
'FREE': 2,
|
|
'STARTER': 5,
|
|
'PROFESSIONAL': 15,
|
|
'ENTERPRISE': 50,
|
|
}
|
|
return tier_defaults.get(self.subscription_tier, 5)
|
|
|
|
def get_effective_max_resources(self):
|
|
"""Get max resources (custom or tier default)"""
|
|
if self.custom_max_resources is not None:
|
|
return self.custom_max_resources
|
|
# Tier defaults
|
|
tier_defaults = {
|
|
'FREE': 3,
|
|
'STARTER': 10,
|
|
'PROFESSIONAL': 30,
|
|
'ENTERPRISE': 100,
|
|
}
|
|
return tier_defaults.get(self.subscription_tier, 10)
|
|
|
|
@classmethod
|
|
def create_invitation(cls, email, invited_by, subscription_tier='PROFESSIONAL',
|
|
suggested_business_name='', custom_max_users=None,
|
|
custom_max_resources=None, permissions=None, limits=None,
|
|
personal_message=''):
|
|
"""
|
|
Create a new tenant invitation, cancelling any existing pending invitations
|
|
for the same email.
|
|
"""
|
|
# Cancel existing pending invitations for this email
|
|
cls.objects.filter(
|
|
email=email,
|
|
status=cls.Status.PENDING
|
|
).update(status=cls.Status.CANCELLED)
|
|
|
|
# Create new invitation
|
|
return cls.objects.create(
|
|
email=email,
|
|
invited_by=invited_by,
|
|
subscription_tier=subscription_tier,
|
|
suggested_business_name=suggested_business_name,
|
|
custom_max_users=custom_max_users,
|
|
custom_max_resources=custom_max_resources,
|
|
permissions=permissions or {},
|
|
limits=limits or {},
|
|
personal_message=personal_message,
|
|
)
|
|
|
|
|
|
class PlatformEmailAddress(models.Model):
|
|
"""
|
|
Platform-managed email addresses hosted on mail.talova.net.
|
|
These are managed directly via SSH/Docker commands on the mail server.
|
|
|
|
Unlike TicketEmailAddress which supports arbitrary IMAP/SMTP servers,
|
|
this model is specifically for platform email addresses managed on our
|
|
dedicated mail server.
|
|
"""
|
|
|
|
class Domain(models.TextChoices):
|
|
SMOOTHSCHEDULE = 'smoothschedule.com', 'smoothschedule.com'
|
|
TALOVA = 'talova.net', 'talova.net'
|
|
|
|
# Display information
|
|
display_name = models.CharField(
|
|
max_length=100,
|
|
help_text="Display name (e.g., 'Support', 'Billing', 'Sales')"
|
|
)
|
|
local_part = models.CharField(
|
|
max_length=64,
|
|
help_text="Local part of email address (before @)"
|
|
)
|
|
domain = models.CharField(
|
|
max_length=50,
|
|
choices=Domain.choices,
|
|
default=Domain.SMOOTHSCHEDULE,
|
|
help_text="Email domain"
|
|
)
|
|
color = models.CharField(
|
|
max_length=7,
|
|
default='#3b82f6',
|
|
help_text="Hex color code for visual identification"
|
|
)
|
|
sender_name = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
default='',
|
|
help_text="Name to show in From header (e.g., 'SmoothSchedule Support'). If blank, uses display_name."
|
|
)
|
|
assigned_user = models.ForeignKey(
|
|
'users.User',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='platform_email_addresses',
|
|
help_text="User associated with this email. If set, their name is used as sender name."
|
|
)
|
|
|
|
# Account credentials (stored securely, synced to mail server)
|
|
password = models.CharField(
|
|
max_length=255,
|
|
help_text="Password for the email account (stored encrypted, synced to mail server)"
|
|
)
|
|
|
|
# Status
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
help_text="Whether this email address is active"
|
|
)
|
|
is_default = models.BooleanField(
|
|
default=False,
|
|
help_text="Default email for platform support"
|
|
)
|
|
|
|
# Mail server sync status
|
|
mail_server_synced = models.BooleanField(
|
|
default=False,
|
|
help_text="Whether the account exists on the mail server"
|
|
)
|
|
last_sync_error = models.TextField(
|
|
blank=True,
|
|
default='',
|
|
help_text="Last sync error message, if any"
|
|
)
|
|
last_synced_at = models.DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
help_text="When this account was last synced to the mail server"
|
|
)
|
|
|
|
# Usage tracking
|
|
emails_processed_count = models.IntegerField(
|
|
default=0,
|
|
help_text="Total number of emails processed for this address"
|
|
)
|
|
last_check_at = models.DateTimeField(
|
|
null=True,
|
|
blank=True,
|
|
help_text="When emails were last checked for this address"
|
|
)
|
|
|
|
# Timestamps
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
app_label = 'platform_admin'
|
|
ordering = ['-is_default', 'display_name']
|
|
unique_together = [['local_part', 'domain']]
|
|
verbose_name = 'Platform Email Address'
|
|
verbose_name_plural = 'Platform Email Addresses'
|
|
|
|
def __str__(self):
|
|
return f"{self.display_name} <{self.email_address}>"
|
|
|
|
@property
|
|
def email_address(self):
|
|
"""Full email address."""
|
|
return f"{self.local_part}@{self.domain}"
|
|
|
|
@property
|
|
def effective_sender_name(self):
|
|
"""
|
|
Name to use in From header.
|
|
Priority: assigned_user's full name > sender_name > display_name
|
|
"""
|
|
if self.assigned_user:
|
|
user_name = self.assigned_user.get_full_name()
|
|
if user_name:
|
|
return user_name
|
|
if self.sender_name:
|
|
return self.sender_name
|
|
return self.display_name
|
|
|
|
def save(self, *args, **kwargs):
|
|
# Ensure only one default
|
|
if self.is_default:
|
|
PlatformEmailAddress.objects.filter(
|
|
is_default=True
|
|
).exclude(pk=self.pk).update(is_default=False)
|
|
super().save(*args, **kwargs)
|
|
|
|
# Pre-configured mail server settings
|
|
MAIL_SERVER_HOST = 'mail.talova.net'
|
|
IMAP_HOST = 'mail.talova.net'
|
|
IMAP_PORT = 993
|
|
IMAP_USE_SSL = True
|
|
SMTP_HOST = 'mail.talova.net'
|
|
SMTP_PORT = 587
|
|
SMTP_USE_TLS = True
|
|
SMTP_USE_SSL = False
|
|
|
|
def get_imap_settings(self):
|
|
"""Get IMAP connection settings."""
|
|
return {
|
|
'host': self.IMAP_HOST,
|
|
'port': self.IMAP_PORT,
|
|
'use_ssl': self.IMAP_USE_SSL,
|
|
'username': self.email_address,
|
|
'password': self.password,
|
|
'folder': 'INBOX',
|
|
}
|
|
|
|
def get_smtp_settings(self):
|
|
"""Get SMTP connection settings."""
|
|
return {
|
|
'host': self.SMTP_HOST,
|
|
'port': self.SMTP_PORT,
|
|
'use_tls': self.SMTP_USE_TLS,
|
|
'use_ssl': self.SMTP_USE_SSL,
|
|
'username': self.email_address,
|
|
'password': self.password,
|
|
}
|