quesurifn

quesurifn

So, I’m building an SMTP client that I’d like to be able to be called from different processes and have different state for each process. The SMTP client needs to track for example if the HELO command has been used or not. How do I handle state?

My current code is this:

defmodule Client do
  @moduledoc """
  Documentation for `Smtpclient`.
  """

  use Agent

  def start_link(_opts) do
    Agent.start_link(fn -> %{} end, name: __MODULE__)
  end


  def connect(pid, tld) do
      set_value(pid, "tld", tld)
      {:ok, records} = DNS.resolve(tld, :mx)
      Enum.sort_by(records, fn(r) -> elem(r, 0) end)
      sock = do_connect(records, 0)
      set_socket(pid, sock)
      set_value(pid, "hostname", tld)
      {:ok, sock}
  end

  @spec helo(any) :: any
  def helo(pid) do
      already_helloed = get_value(pid, "already_helloed")
      if already_helloed do # if already helloed return early
        {:error, "already helloed"}
      end
      sock = get_socket(pid)
      tld = get_value(pid, "tld")
      sock |> Socket.Stream.send!("HELO #{tld}")
      set_value(pid, "already_helloed", true)
      sock |> Socket.Stream.recv!
  end

  def noop(pid) do
    try do
      sock = get_socket(pid)
      sock |> Socket.Stream.send!("NOOP")
      sock |> Socket.Stream.recv!
    rescue
      Socker.Error -> {:error, :no_connection}
    end
  end

  def quit(sock) do
      try do
        sock |> Socket.Stream.send!("QUIT")
        sock |> Socket.Stream.recv!
        sock |> Socket.close!()
      rescue
        Socket.Error -> {:error, "something is wrong"}
      end
  end

  def rctp_to(pid, email) do
    try do
      sock = get_socket(pid)
      sock |> Socket.Stream.send!("RCPT_TO #{email}")
      sock |> Socket.Stream.recv!
    rescue
      Socker.Error -> {:error, "something is wrong"}
    end
  end

  defp set_socket(pid, sock) do
    Agent.update(pid, &Map.put(&1, "socket", sock))
  end

  defp get_socket(pid) do
    Agent.get(pid, &Map.get(&1, "socket"))
  end

  defp set_value(pid, key, value) do
    Agent.update(pid, &Map.put(&1, key, value))
  end

  defp get_value(pid, key) do
    Agent.get(pid, &Map.get(&1, key))
  end

  defp do_connect(hostnames, i) do
    try do
      host = elem(Enum.at(hostnames, i), 1)
      Socket.TCP.connect!(to_string(host), 25, packet: :line)
    rescue
        Socket.Error ->
          if Enum.at(hostnames, i) == nil do
            {:error, "all mx servers down"}
          end
          do_connect(hostnames, i + 1)
    end
  end
end

Showing Posts 1 to 5

hauleth

hauleth

You probably should use :gen_statem if you want state machine. It will make a lot of stuff much easier. Additionally if you want SMTP client/server then there is already implementation of that in Erlang

https://github.com/gen-smtp/gen_smtp

You can use that either as an implementation or as inspiration.

Sebb

Sebb

I find :gen_statem very hard to understand. I prefer functional fsm with the state in map/struct and pattern matching in multiclauses as handlers. Also because fsm can become hard to test if they have side effects (like timeouts) I think its worth to think about how to make them as pure as possible.

IloSophiep

IloSophiep

I think there are two “default” ways to handle state (and a lot more specific ones):

  • Store them in the client process
  • Store them in a dedicated process

Because Elixir is really good with processes people sometimes expect everything they code to do “process-y” stuff. But if you can, i think, it is recommended to start simple and do not introduce processes if you don’t need to. Your first sentence makes me think, that might work for you.

As an example: You have a module TodoList with a function add/1 that adds one item to the list. You might think “How / Where do i keep that list, so calling add/1 three times keeps all the old entries still there?” The easiest answer is "Just return the data to the caller and let them keep the state around! So instead of

defmodule TodoList do
  def add(item) do
    # Where does before come from?!
    [ item | before ]
    :ok
  end
end

you use

defmodule TodoList do
  def add(state, item) do
    # renamed "before" to "state", so the
    # concept hopefully becomes clearer
    [ item | state ]
  end
end

so the caller of your client keeps the state around and hands it to you for you to work on it.

quesurifn

quesurifn OP

I might be wrong but the SMTP client requires a relay. I want a client that can delivery without a relay.

hauleth

hauleth

SMTP Relay for client is just an regular SMTP Server. So if you can deliver via relay then you can deliver directly (as soon as you can do so in a way that will not cause your email to be rejected/marked as spam automatically).

— All posts loaded —

Where Next? Top

Trending in Chat/Questions Top

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews