#!/usr/bin/env ruby require 'toml' require 'net/ldap' require 'English' require 'open3' @mailman3_exec = '/usr/bin/mailman' @configuration_file = '/etc/mailman3-ldap-sync.toml' @configuration = nil def read_configuration(path) begin TOML.load_file(path) rescue Errno::ENOENT => e puts "Could not open configuration file: #{e}" exit(1) end end def connect_ldap conn = Net::LDAP.new( host: @configuration['ldap']['host'], port: @configuration['ldap']['port'], encryption: :simple_tls, auth: { method: :simple, username: @configuration['ldap']['auth']['username'], password: @configuration['ldap']['auth']['password'] } ) ldap_addr = "#{@configuration['ldap']['host']}:#{@configuration['ldap']['port']}" begin if conn.bind conn else puts "Failed to authenticate against LDAP server: #{ldap_addr}" exit(1) end rescue Net::LDAP::LdapError => e puts "Failed to contact LDAP server: #{ldap_addr}" puts e exit(1) end end def get_mailman3_subscribers(list) unless File.executable?(@mailman3_exec) puts "could not execute #{@mailman3_exec}" exit(1) end # Execute mailman and check whether it exited successfuly raw = `#{@mailman3_exec} members #{list}` unless $CHILD_STATUS.exitstatus.zero? puts 'FAIL' exit(1) end # Return a list of mail addresses. raw.split("\n") end def add_subscribers_to(list, addresses) # The address is not subscribed to the list yet. print "Adding #{addresses.size} addresses... " # Subscribe the address to the list. Open3.popen3(@mailman3_exec, 'members', '-a', '-', list) do |stdin, stdout, _stderr, wait_thr| stdin.write(addresses.join("\n")) stdin.close_write process = wait_thr.value if process.exitstatus.zero? puts 'OK' else puts 'FAILED' end puts '> Mailman output:' puts stdout.read end end def sync_list(list_name, ldap_group_entry, ldap_conn) user_basetree = @configuration['ldap']['users']['basetree'] list = "#{list_name}@#{@configuration['domain']}" ldap_members = ldap_group_entry.memberuid.map do |uid| filter = Net::LDAP::Filter.eq('uid', uid) matched_ldap_users = ldap_conn.search(base: user_basetree, filter: filter) if matched_ldap_users.nil? || matched_ldap_users.empty? '' else user = matched_ldap_users.first address = user.mail.first # 'mail' is an array for some reason. address end end ldap_members.select! {|e| not e.empty? } puts "Found #{ldap_group_entry.dn} with #{ldap_members.size} members" # Extract mail addresses from Mailman print 'Searchin mailman... ' mailman3_subscribers = get_mailman3_subscribers(list) puts 'OK' puts "Found #{list} with #{mailman3_subscribers.size} subscribers" # Add found addresses to the list. add_subscribers_to(list, ldap_members) end def main # Parse configuration, bind to LDAP server configuration_file = ARGV[0] ? ARGV[0] : @configuration_file @configuration = read_configuration(configuration_file) ldap_conn = connect_ldap domain = @configuration['domain'] list_basetree = @configuration['ldap']['lists']['basetree'] lists = @configuration['lists'] # Sync mailman3 lists with LDAP groups lists.each do |list_name| puts ">> Processing list #{list_name}@#{domain}" print 'Searching LDAP... ' filter = Net::LDAP::Filter.eq('cn', list_name) matched_ldap_groups = ldap_conn.search(base: list_basetree, filter: filter) if matched_ldap_groups.nil? || matched_ldap_groups.empty? # Could not find matching LDAP group puts 'NOT FOUND' else puts 'OK' ldap_group_entry = matched_ldap_groups.first sync_list(list_name, ldap_group_entry, ldap_conn) end end end ### # Execute script: main