Sources: a packages source only reads the owner's own mailbox

04935c3 · Gabe Morell · 2026-11-04 22:43

10 files +67 -3
Message
{commit_body(@commit)}

Files changed

modified apps/room_sanctum/lib/room_sanctum/configuration.ex
+3 −3
@@ -109,10 +109,10 @@ defmodule RoomSanctum.Configuration do
109 109 """
110 110 def create_source(attrs \\ %{}) do
111 111 r = %Source{} |> Source.changeset(attrs) |> Repo.insert()
112 − {:ok, d} = r
113 112
114 − case d.type do
115 − :packages ->
113 + # A refused source is handed back as one, not raised as a MatchError.
114 + case r do
115 + {:ok, %Source{type: :packages} = d} ->
116 116 ## create main mailbox
117 117 create_taxid(%{source_id: d.id, user: Ecto.UUID.generate(), designator: "mail_main"})
118 118 ## create mailbox for usps handling
modified apps/room_sanctum/lib/room_sanctum/configuration/source.ex
+5 −0
@@ -3,6 +3,8 @@ defmodule RoomSanctum.Configuration.Source do
3 3 import Ecto.Changeset
4 4 import PolymorphicEmbed
5 5
6 + alias RoomSanctum.Configuration.Ownership
7 +
6 8 schema "cfg_sources" do
7 9 has_many :mailboxes, RoomSanctum.Configuration.Taxid
8 10 has_many :webhooks, RoomSanctum.Configuration.Agyr
@@ -83,6 +85,9 @@ defmodule RoomSanctum.Configuration.Source do
83 85 |> foreign_key_constraint(:user_id)
84 86 |> cast_polymorphic_embed(:config, required: true)
85 87 |> validate_required([:name, :type, :enabled, :user_id])
88 + # A packages source is fed the mail its mailbox source pulls, so it may
89 + # only name one of its owner's own.
90 + |> Ownership.validate_owned_in(:config, :mailbox_source_id, __MODULE__)
86 91 end
87 92
88 93 def meta_changeset(meta, attrs \\ %{}) do
added apps/room_sanctum/test/room_sanctum/source_mailbox_owner_test.exs
+59 −0
@@ -0,0 +1,59 @@
1 +defmodule RoomSanctum.SourceMailboxOwnerTest do
2 + @moduledoc """
3 + A packages source is fed the mail its mailbox source pulls, so it may only
4 + name one of its owner's own -- naming someone else's would fan their mail
5 + out to it.
6 + """
7 + use RoomSanctum.DataCase
8 +
9 + import RoomSanctum.OwnershipFixtures
10 +
11 + alias RoomSanctum.Configuration
12 +
13 + defp mailbox_for(user) do
14 + source_for(user, %{
15 + type: :mailbox,
16 + config: %{
17 + "__type__" => "mailbox",
18 + "host" => "imap.e.test",
19 + "username" => "me",
20 + "password" => "pw"
21 + }
22 + })
23 + end
24 +
25 + defp packages(user, mailbox) do
26 + Configuration.create_source(%{
27 + name: "Parcels",
28 + notes: "",
29 + type: :packages,
30 + enabled: true,
31 + user_id: user.id,
32 + config: %{"__type__" => "packages", "mailbox_source_id" => mailbox.id}
33 + })
34 + end
35 +
36 + test "the owner's own mailbox is accepted" do
37 + me = owner()
38 + mailbox = mailbox_for(me)
39 + assert {:ok, source} = packages(me, mailbox)
40 + assert source.config.mailbox_source_id == mailbox.id
41 + end
42 +
43 + test "another user's mailbox is refused, as a changeset error" do
44 + assert {:error, changeset} = packages(owner(), mailbox_for(owner()))
45 + assert %{config: [_]} = errors_on(changeset)
46 + end
47 +
48 + test "an existing source cannot be moved onto another user's mailbox" do
49 + me = owner()
50 + {:ok, source} = packages(me, mailbox_for(me))
51 + theirs = mailbox_for(owner())
52 +
53 + assert {:error, _} =
54 + Configuration.update_source_config(source, %{mailbox_source_id: theirs.id})
55 +
56 + assert {:ok, _} = Configuration.update_source(source, %{name: "Renamed"})
57 + assert {:ok, _} = Configuration.update_source_config(source, %{handle_usps: false})
58 + end
59 +end

Parents: b4aa1b1