bindy/bind9/records/
mx.rs

1// Copyright (c) 2025 Erick Bourgeois, firestoned
2// SPDX-License-Identifier: MIT
3
4//! MX record management.
5
6use super::super::types::RndcKeyData;
7use super::{build_authenticated_client, build_record_fqdn, should_update_record};
8use anyhow::Result;
9use hickory_net::client::ClientHandle;
10use hickory_proto::op::ResponseCode;
11use hickory_proto::rr::{rdata, DNSClass, Name, RData, Record, RecordType};
12use std::str::FromStr;
13use tracing::info;
14
15use crate::constants::DEFAULT_DNS_RECORD_TTL_SECS;
16
17/// Add an MX record using dynamic DNS update (RFC 2136).
18///
19/// # Errors
20///
21/// Returns an error if the DNS update fails or the server rejects it.
22#[allow(clippy::too_many_arguments)]
23pub async fn add_mx_record(
24    zone_name: &str,
25    name: &str,
26    priority: i32,
27    mail_server: &str,
28    ttl: Option<i32>,
29    server: &str,
30    key_data: &RndcKeyData,
31) -> Result<()> {
32    let mail_server_for_comparison = mail_server.to_string();
33    let priority_u16 = u16::try_from(priority).unwrap_or(10);
34    let should_update = should_update_record(
35        zone_name,
36        name,
37        RecordType::MX,
38        "MX",
39        server,
40        |existing_records| {
41            if existing_records.len() == 1 {
42                if let RData::MX(existing_mx) = &existing_records[0].data {
43                    return existing_mx.preference == priority_u16
44                        && existing_mx.exchange.to_string() == mail_server_for_comparison;
45                }
46            }
47            false
48        },
49    )
50    .await?;
51
52    if !should_update {
53        return Ok(());
54    }
55
56    let ttl_value = u32::try_from(ttl.unwrap_or(DEFAULT_DNS_RECORD_TTL_SECS))
57        .unwrap_or(u32::try_from(DEFAULT_DNS_RECORD_TTL_SECS).unwrap_or(300));
58
59    let zone = Name::from_str(zone_name)?;
60    let fqdn = build_record_fqdn(zone_name, name)?;
61    let mx_name = Name::from_str(mail_server)?;
62
63    let mut record = Record::from_rdata(
64        fqdn.clone(),
65        ttl_value,
66        RData::MX(rdata::MX::new(priority_u16, mx_name)),
67    );
68    record.dns_class = DNSClass::IN;
69
70    info!(
71        "Adding MX record: {} -> {} (priority: {}, TTL: {})",
72        fqdn, mail_server, priority_u16, ttl_value
73    );
74
75    let mut client = build_authenticated_client(server, key_data).await?;
76    let response = client.append(record, zone, false).await?;
77
78    match response.metadata.response_code {
79        ResponseCode::NoError => {
80            info!("Successfully added MX record: {} -> {}", name, mail_server);
81            Ok(())
82        }
83        code => Err(anyhow::anyhow!(
84            "DNS update failed with response code: {code:?}"
85        )),
86    }
87}
88
89#[cfg(test)]
90#[path = "mx_tests.rs"]
91mod mx_tests;