lib/gandi_v5/email/mailbox.rb
# frozen_string_literal: true
class GandiV5
class Email
# A mailbox that lives within a domain.
# @!attribute [r] address
# @return [String] full email address.
# @!attribute [r] fqdn
# @return [String] domain name.
# @!attribute [r] uuid
# @return [String]
# @!attribute [r] login
# @return [String] mailbox login.
# @!attribute [r] type
# @return [:standard, :premium, :free]
# @!attribute [r] quota_used
# @return [Integer]
# @!attribute [r] aliases
# @return [nil, Array<String>] mailbox alias list.
# A local-part (what comes before the "@") of an email address. It can contain a wildcard
# "*" before or after at least two characters to redirect everything thats matches the
# local-part pattern.
# @!attribute [r] fallback_email
# @return [nil, String] fallback email addresse.
# @!attribute [r] responder
# @return [nil, GandiV5::Email::Mailbox::Responder]
class Mailbox
include GandiV5::Data
TYPES = %i[standard premium free].freeze
QUOTAS = {
free: 3 * (1024**3),
standard: 3 * (1024**3),
premium: 50 * (1024**3)
}.freeze
members :address, :login, :quota_used, :aliases, :fallback_email
member :type, gandi_key: 'mailbox_type', converter: GandiV5::Data::Converter::Symbol
member :uuid, gandi_key: 'id'
member :fqdn, gandi_key: 'domain'
member :responder, converter: GandiV5::Email::Mailbox::Responder
alias mailbox_uuid uuid
# Create a new GandiV5::Email::Mailbox
# @param members [Hash{Symbol => Object}]
# @return [GandiV5::Email::Slot]
def initialize(**members)
super(**members)
responder.instance_exec(self) { |mb| @mailbox = mb } if responder?
end
# Delete the mailbox and it's contents.
# If you delete a mailbox for which you have purchased a slot,
# this action frees the slot so it once again becomes available
# for use with a new mailbox, or for deletion.
# @see https://api.gandi.net/docs/email#delete-v5-email-mailboxes-domain-mailbox_id
# @return [String] The confirmation message from Gandi.
# @raise [GandiV5::Error::GandiError] if Gandi returns an error.
def delete
_response, data = GandiV5.delete url
data['message']
end
# Purge the contents of the mailbox.
# @see https://api.gandi.net/docs/email#delete-v5-email-mailboxes-domain-mailbox_id-contents
# @return [String] The confirmation message from Gandi.
# @raise [GandiV5::Error::GandiError] if Gandi returns an error.
def purge
_response, data = GandiV5.delete "#{url}/contents"
data['message']
end
# Requery Gandi fo this mailbox's information.
# @return [GandiV5::Email::Mailbox]
# @raise [GandiV5::Error::GandiError] if Gandi returns an error.
def refresh
_response, data = GandiV5.get url
from_gandi data
responder.instance_exec(self) { |mb| @mailbox = mb } if responder?
self
end
# Update the mailbox's settings.
# @see https://api.gandi.net/docs/email#patch-v5-email-mailboxes-domain-mailbox_id
# @param login [String, #to_s] the login name (and first part of email address).
# @param password [String, #to_s] the password to use.
# @param aliases [Array<String, #to_s>] any alternative email address to be used.
# @param responder [Hash, GandiV5::Mailbox::Responder, #to_gandi, #to_h]
# auto responder settings.
# @return [String] The confirmation message from Gandi.
# @raise [GandiV5::Error::GandiError] if Gandi returns an error.
def update(**body)
return 'Nothing to update.' if body.empty?
check_password body[:password] if body.key?(:password)
body[:password] = crypt_password(body[:password]) if body.key?(:password)
if (responder = body[:responder])
body[:responder] = responder.respond_to?(:to_gandi) ? responder.to_gandi : responder.to_h
end
_response, data = GandiV5.patch url, body.to_json
refresh
data['message']
end
# Upgrade a standard mailbox to premium.
# If the current slot is a free one, a new premium slot is created and
# used for the mailbox. Otherwise, the slot is upgraded to premium.
# @see https://api.gandi.net/docs/email#patch-v5-email-mailboxes-domain-mailbox_id-type
# @param sharing_id [String, #to_s, nil] (optional)
# the organisation ID to bill for the mailbox.
# @param dry_run [Boolean] whether the details should be checked instead
# of actually upgrading the mailbox.
# @return [true] if the mailbox was upgraded
# @return [false] if the mailbox was not upgraded (it's already premium)
# @return [Hash] if doing a dry run, you get what Gandi returns
# @raise [GandiV5::Error::GandiError] if Gandi returns an error
def upgrade(sharing_id: nil, dry_run: false)
patch_type :premium, sharing_id, dry_run
end
# Downgrade a premium mailbox to standard.
# If a free slot is available, the premium slot is destroyed
# (and refunded) and the free one is used for the mailbox.
# Otherwise, the slot is downgraded to standard.
# @see https://api.gandi.net/docs/email#patch-v5-email-mailboxes-domain-mailbox_id-type
# @param sharing_id [String, #to_s, nil] (optional)
# the organisation ID to bill for the mailbox.
# @param dry_run [Boolean] whether the details should be checked instead
# of actually downgrading the mailbox.
# @return [true] if the mailbox was downgraded
# @return [false] if the mailbox was not downgraded (it's already standard)
# @return [Hash] if doing a dry run, you get what Gandi returns
# @raise [GandiV5::Error::GandiError] if Gandi returns an error
def downgrade(sharing_id: nil, dry_run: false)
patch_type :standard, sharing_id, dry_run
end
# Create a new mailbox.
# Note that before you can create a mailbox, you must have a slot available.
# @see https://api.gandi.net/docs/email#post-v5-email-mailboxes-domain
# @param fqdn [String, #to_s] the fully qualified domain name for the mailbox.
# @param login [String, #to_s] the login name (and first part of email address).
# @param password [String, #to_s] the password to use.
# @param aliases [Array<String, #to_s>] any alternative email address to be used.
# @param type [:standard, :premium] the type of mailbox slot to use.
# @param dry_run [Boolean] whether the details should be checked instead
# of actually creating the mailbox.
# @return [GandiV5::Email::Mailbox] The created mailbox.
# @raise [GandiV5::Error] if no slots are available.
# @raise [GandiV5::Error::GandiError] if Gandi returns an error.
# rubocop:disable Metrics/AbcSize
def self.create(fqdn, login, password, aliases: [], type: :standard, dry_run: false)
fail ArgumentError, "#{type.inspect} is not a valid type" unless TYPES.include?(type)
if GandiV5::Email::Slot.list.none? { |slot| slot.mailbox_type == type && slot.inactive? }
fail GandiV5::Error, "no available #{type} slots"
end
check_password password
body = {
mailbox_type: type,
login: login,
password: crypt_password(password),
aliases: aliases.push
}.to_json
response, data = GandiV5.post(url(fqdn), body, 'Dry-Run': dry_run ? 1 : 0)
dry_run ? data : fetch(fqdn, response.headers[:location].split('/').last)
end
# rubocop:enable Metrics/AbcSize
# Get information for a mailbox.
# @see https://api.gandi.net/docs/email#get-v5-email-mailboxes-domain-mailbox_id
# @param fqdn [String, #to_s] the fully qualified domain name for the mailbox.
# @param uuid [String, #to_s] unique identifier of the mailbox.
# @return [GandiV5::Email::Mailbox]
# @raise [GandiV5::Error::GandiError] if Gandi returns an error.
def self.fetch(fqdn, uuid)
_response, data = GandiV5.get url(fqdn, uuid)
from_gandi data
end
# List mailboxes for a domain.
# @see https://api.gandi.net/docs/email#get-v5-email-mailboxes-domain
# @param fqdn [String, #to_s] the fully qualified domain name for the mailboxes.
# @param page [Integer, #each<Integer>] which page(s) of results to get.
# If page is not provided keep querying until an empty list is returned.
# If page responds to .each then iterate until an empty list is returned.
# @param per_page [Integer, #to_s] (optional default 100) how many results ot get per page.
# @param sort_by [#to_s] (optional default "login")
# how to sort the results ("login", "-login").
# @param login [String] (optional) filter the list by login (pattern)
# e.g. ("alice" "*lice", "alic*").
# @return [Array<GandiV5::Email::Mailbox>]
# @raise [GandiV5::Error::GandiError] if Gandi returns an error.
def self.list(fqdn, page: (1..), per_page: 100, **params)
params['~login'] = params.delete(:login)
params.compact! { |_k, v| v.nil? }
mailboxes = []
GandiV5.paginated_get(url(fqdn), page, per_page, params: params) do |data|
mailboxes += data.map { |mailbox| from_gandi mailbox }
end
mailboxes
end
# Get the quota for this type of mailbox.
# @return [Integer] bytes.
def quota
QUOTAS[type]
end
# Get the quota usage for this mailbox.
# @return [Float] fraction of quota used (typically between 0.0 and 1.0)
def quota_usage
quota_used.to_f / quota
end
# Returns the string representation of the mailbox.
# Includes the type, address, quota usage, activeness of responder (if present)
# and aliases (if present).
# @return [String]
def to_s
s = "[#{type}] #{address} (#{quota_used}/#{quota} (#{(quota_usage * 100).round}%))"
s += " with #{responder.active? ? 'active' : 'inactive'} responder" if responder
s += " aka: #{aliases.join(', ')}" if aliases&.any?
s
end
private
# rubocop:disable Style/GuardClause
def self.check_password(password)
if !(9..200).cover?(password.length)
fail ArgumentError, 'password must be between 9 and 200 characters'
elsif password.count('A-Z') < 1
fail ArgumentError, 'password must contain at least one upper case character'
elsif password.count('0-9') < 3
fail ArgumentError, 'password must contain at least three numbers'
elsif password.count('^a-z^A-Z^0-9') < 1
fail ArgumentError, 'password must contain at least one special character'
end
end
private_class_method :check_password
# rubocop:enable Style/GuardClause
def check_password(password)
self.class.send :check_password, password
end
def url
"#{BASE}email/mailboxes/#{CGI.escape fqdn}/#{CGI.escape uuid}"
end
def self.url(fqdn, uuid = nil)
"#{BASE}email/mailboxes/#{CGI.escape fqdn}" +
(uuid ? "/#{CGI.escape uuid}" : '')
end
private_class_method :url
def self.crypt_password(password)
# You can also send a hashed password in sha512-crypt ie: {SHA512-CRYPT}$6$xxxx$yyyy
salt = SecureRandom.random_number(36**8).to_s(36)
password.crypt("$6$#{salt}")
end
private_class_method :crypt_password
def crypt_password(password)
self.class.send :crypt_password, password
end
def patch_type(new_type, sharing_id, dry_run)
fail ArgumentError unless TYPES.include?(new_type)
return false if type == new_type
url_ = "#{url}/type"
url_ = sharing_id ? "#{url_}?sharing_id=#{sharing_id}" : url_
body = { mailbox_type: new_type }
_response, data = GandiV5.patch(url_, body.to_json, 'Dry-Run': dry_run ? 1 : 0)
@type = new_type unless dry_run
dry_run ? data : true
end
end
end
end