Class: MisarMail::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/misar_mail/client.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_key:, timeout: 30, max_retries: 3, base_url: BASE_URL) ⇒ Client

Returns a new instance of Client.

Parameters:

  • base_url (String) (defaults to: BASE_URL)

    override the API host — point it at a stub server in tests, or at a self-hosted deployment. Defaults to BASE_URL.



593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
# File 'lib/misar_mail/client.rb', line 593

def initialize(api_key:, timeout: 30, max_retries: 3, base_url: BASE_URL)
  @api_key     = api_key
  @timeout     = timeout
  @max_retries = max_retries
  @base_url    = base_url

  @plan          = PlanResource.new(self)
  @streaming     = StreamingResource.new(self)
  @ai = AiResource.new(self)
  @credit_rates = CreditRatesResource.new(self)
  @deliverability = DeliverabilityResource.new(self)
  @dmarc = DmarcResource.new(self)
  @email_accounts = EmailAccountsResource.new(self)
  @emails = EmailsResource.new(self)
  @landing_pages = LandingPagesResource.new(self)
  @monetization = MonetizationResource.new(self)
  @revenue = RevenueResource.new(self)
  @segments = SegmentsResource.new(self)
  @subscription = SubscriptionResource.new(self)
  @team_members = TeamMembersResource.new(self)
  @wallet = WalletResource.new(self)
  @warmup = WarmupResource.new(self)
  @email         = EmailResource.new(self)
  @contacts      = ContactsResource.new(self)
  @campaigns     = CampaignsResource.new(self)
  @templates     = TemplatesResource.new(self)
  @automations   = AutomationsResource.new(self)
  @domains       = DomainsResource.new(self)
  @dedicated_ips = DedicatedIpsResource.new(self)
  @ab_tests      = AbTestsResource.new(self)
  @sandbox       = SandboxResource.new(self)
  @inbound       = InboundResource.new(self)
  @analytics     = AnalyticsResource.new(self)
  @track         = TrackResource.new(self)
  @keys          = KeysResource.new(self)
  @validate      = ValidateResource.new(self)
  @webhooks      = WebhooksResource.new(self)
  @usage         = UsageResource.new(self)
  @billing       = BillingResource.new(self)
end

Instance Attribute Details

#ab_testsObject (readonly)

Returns the value of attribute ab_tests.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def ab_tests
  @ab_tests
end

#aiObject (readonly)

Returns the value of attribute ai.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def ai
  @ai
end

#analyticsObject (readonly)

Returns the value of attribute analytics.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def analytics
  @analytics
end

#automationsObject (readonly)

Returns the value of attribute automations.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def automations
  @automations
end

#billingObject (readonly)

Returns the value of attribute billing.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def billing
  @billing
end

#campaignsObject (readonly)

Returns the value of attribute campaigns.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def campaigns
  @campaigns
end

#contactsObject (readonly)

Returns the value of attribute contacts.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def contacts
  @contacts
end

#credit_ratesObject (readonly)

Returns the value of attribute credit_rates.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def credit_rates
  @credit_rates
end

#dedicated_ipsObject (readonly)

Returns the value of attribute dedicated_ips.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def dedicated_ips
  @dedicated_ips
end

#deliverabilityObject (readonly)

Returns the value of attribute deliverability.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def deliverability
  @deliverability
end

#dmarcObject (readonly)

Returns the value of attribute dmarc.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def dmarc
  @dmarc
end

#domainsObject (readonly)

Returns the value of attribute domains.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def domains
  @domains
end

#emailObject (readonly)

Returns the value of attribute email.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def email
  @email
end

#email_accountsObject (readonly)

Returns the value of attribute email_accounts.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def email_accounts
  @email_accounts
end

#emailsObject (readonly)

Returns the value of attribute emails.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def emails
  @emails
end

#inboundObject (readonly)

Returns the value of attribute inbound.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def inbound
  @inbound
end

#keysObject (readonly)

Returns the value of attribute keys.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def keys
  @keys
end

#landing_pagesObject (readonly)

Returns the value of attribute landing_pages.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def landing_pages
  @landing_pages
end

#monetizationObject (readonly)

Returns the value of attribute monetization.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def monetization
  @monetization
end

#planObject (readonly)

Returns the value of attribute plan.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def plan
  @plan
end

#revenueObject (readonly)

Returns the value of attribute revenue.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def revenue
  @revenue
end

#sandboxObject (readonly)

Returns the value of attribute sandbox.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def sandbox
  @sandbox
end

#segmentsObject (readonly)

Returns the value of attribute segments.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def segments
  @segments
end

#streamingObject (readonly)

Returns the value of attribute streaming.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def streaming
  @streaming
end

#subscriptionObject (readonly)

Returns the value of attribute subscription.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def subscription
  @subscription
end

#team_membersObject (readonly)

Returns the value of attribute team_members.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def team_members
  @team_members
end

#templatesObject (readonly)

Returns the value of attribute templates.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def templates
  @templates
end

#trackObject (readonly)

Returns the value of attribute track.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def track
  @track
end

#usageObject (readonly)

Returns the value of attribute usage.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def usage
  @usage
end

#validateObject (readonly)

Returns the value of attribute validate.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def validate
  @validate
end

#walletObject (readonly)

Returns the value of attribute wallet.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def wallet
  @wallet
end

#warmupObject (readonly)

Returns the value of attribute warmup.



584
585
586
# File 'lib/misar_mail/client.rb', line 584

def warmup
  @warmup
end

#webhooksObject (readonly)

Returns the value of attribute webhooks.



585
586
587
# File 'lib/misar_mail/client.rb', line 585

def webhooks
  @webhooks
end

Instance Method Details

#request(method, path, data = {}, base = nil) ⇒ Hash

Parameters:

  • method (Symbol)

    :get, :post, :patch, :put, :delete

  • path (String)

    e.g. "/contacts"

  • data (Hash) (defaults to: {})

    request body (post/put/patch only)

  • base (String) (defaults to: nil)

    override base URL (for billing/workspaces)

Returns:

  • (Hash)

Raises:



640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
# File 'lib/misar_mail/client.rb', line 640

def request(method, path, data = {}, base = nil)
  base ||= @base_url
  url      = URI.parse(base.chomp("/") + "/" + path.delete_prefix("/"))
  has_body = !data.empty? && %i[post put patch].include?(method)
  json_body = has_body ? JSON.generate(data) : nil

  last_status = 0

  @max_retries.times do |attempt|
    begin
      http              = Net::HTTP.new(url.host, url.port)
      http.use_ssl      = url.scheme == "https"
      http.open_timeout = 10
      http.read_timeout = @timeout

      req = build_request(method, url, json_body)
      resp = http.request(req)
      last_status = resp.code.to_i

      # A rate-limit 429 and a spent-allowance 429 are identical by
      # status, so the body decides. Only the first is worth retrying.
      if plan_limit?(resp)
        raise PlanLimitError.new(
          last_status,
          plan_limit_message(resp),
          parse_body(resp),
          response_headers(resp),
        )
      end

      if RETRYABLE.include?(last_status) && attempt < @max_retries - 1
        sleep(RETRY_BASE_S * (2**attempt))
        next
      end

      return parse_response(resp, last_status)

    rescue Net::OpenTimeout, Net::ReadTimeout,
           Errno::ECONNREFUSED, Errno::ECONNRESET, SocketError => e
      if attempt < @max_retries - 1
        sleep(RETRY_BASE_S * (2**attempt))
        next
      end
      raise NetworkError.new(e.message, e)
    end
  end

  raise ApiError.new(last_status, "Max retries exceeded")
end

#sse_stream(method, path, data = nil) {|frame| ... } ⇒ Enumerator

Opens an SSE connection and yields each decoded frame.

Deliberately not retried: replaying a stream that failed mid-flight would duplicate whatever the caller already consumed.

Yield Parameters:

Returns:

  • (Enumerator)

    when no block is given



698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
# File 'lib/misar_mail/client.rb', line 698

def sse_stream(method, path, data = nil, &block)
  return enum_for(:sse_stream, method, path, data) unless block_given?

  url = URI.parse(API_BASE + path)
  http = Net::HTTP.new(url.host, url.port)
  http.use_ssl = url.scheme == "https"
  http.read_timeout = @timeout

  req = build_request(method, url, data.nil? ? nil : JSON.generate(data))
  req["Accept"] = "text/event-stream"

  http.request(req) do |resp|
    status = resp.code.to_i
    unless (200..299).cover?(status)
      raw = resp.read_body.to_s
      body = begin
        JSON.parse(raw)
      rescue JSON::ParserError
        {}
      end
      if body["code"] == "plan_limit_exceeded" || body["upgrade"].is_a?(Hash)
        raise PlanLimitError.new(status, body["error"] || "plan limit exceeded",
                                 body, response_headers(resp))
      end
      raise ApiError.new(status, body["error"] || (raw.empty? ? "stream error" : raw))
    end

    buffer = +""
    event_name = nil
    data_lines = []

    flush = lambda do
      next nil if data_lines.empty?

      raw = data_lines.join("\n")
      name = event_name
      data_lines = []
      event_name = nil
      next :done if raw == "[DONE]"

      decoded = begin
        JSON.parse(raw)
      rescue JSON::ParserError
        nil
      end
      StreamEvent.new(name, decoded, raw)
    end

    resp.read_body do |chunk|
      buffer << chunk
      # A frame ends at a blank line; split on it and keep the remainder.
      while (idx = buffer.index("\n\n") || buffer.index("\r\n\r\n"))
        frame = buffer.slice!(0, idx)
        buffer.sub!(/\A(\r?\n){1,2}/, "")

        frame.split(/\r?\n/).each do |line|
          next if line.empty? || line.start_with?(":") # keepalive

          if line.start_with?("event:")
            event_name = line.delete_prefix("event:").strip
          elsif line.start_with?("data:")
            data_lines << line.delete_prefix("data:").delete_prefix(" ")
          end
        end

        result = flush.call
        return if result == :done

        yield result if result
      end
    end

    # A trailing frame with no closing blank line.
    buffer.split(/\r?\n/).each do |line|
      next if line.empty? || line.start_with?(":")

      if line.start_with?("event:")
        event_name = line.delete_prefix("event:").strip
      elsif line.start_with?("data:")
        data_lines << line.delete_prefix("data:").delete_prefix(" ")
      end
    end
    tail = flush.call
    yield tail if tail && tail != :done
  end
end